@@ -6,8 +6,11 @@ import * as HostProcess from "@t3tools/shared/HostProcess";
66import { symlinksSupported } from "@t3tools/shared/testing/symlinks" ;
77import * as Effect from "effect/Effect" ;
88import * as FileSystem from "effect/FileSystem" ;
9+ import * as Layer from "effect/Layer" ;
910import * as Path from "effect/Path" ;
11+ import * as PubSub from "effect/PubSub" ;
1012
13+ import * as ProviderInstanceRegistry from "../provider/ProviderInstanceRegistry.ts" ;
1114import * as ServerSettings from "../serverSettings.ts" ;
1215import * as SkillsLibrary from "./SkillsLibrary.ts" ;
1316
@@ -111,19 +114,52 @@ it.layer(NodeServices.layer)("SkillsLibrary", (it) => {
111114 "moves neither copy of a skill whose copies differ and reports both paths" ,
112115 ( ) =>
113116 Effect . gen ( function * ( ) {
114- const { path, claude, codex, library, service, skill, read, link } = yield * setup ( ) ;
117+ const { fs, path, home, claude, codex, library, service, skill, read, link } =
118+ yield * setup ( ) ;
115119 yield * skill ( claude , "review" , "claude version" ) ;
116120 yield * skill ( codex , "review" , "codex version" ) ;
117121 yield * skill ( codex , "other" , "other" ) ;
122+ // One folder on a case-insensitive disk, two on a case-sensitive one.
123+ yield * skill ( claude , "Deploy" , "same" ) ;
124+ yield * skill ( codex , "deploy" , "same" ) ;
125+ // Moving it would break a link that climbs out of the skill.
126+ yield * fs . makeDirectory ( path . join ( claude , "linked-out" ) ) ;
127+ yield * fs . writeFileString ( path . join ( home , ".claude" , "shared.md" ) , "shared" ) ;
128+ yield * fs . symlink ( "../../shared.md" , path . join ( claude , "linked-out" , "SKILL.md" ) ) ;
129+ // Codex's equal copy would be parked at a path the user already uses.
130+ yield * skill ( claude , "dup" , "same" ) ;
131+ yield * skill ( codex , "dup" , "same" ) ;
132+ yield * skill ( codex , ".dup.t3-discard" , "mine" ) ;
118133
119134 yield * service . apply ;
120135
121136 expect ( yield * read ( claude , "review" ) ) . toContain ( "claude version" ) ;
122137 expect ( yield * read ( codex , "review" ) ) . toContain ( "codex version" ) ;
123138 expect ( yield * link ( claude , "review" ) ) . toBeUndefined ( ) ;
124139 expect ( yield * link ( claude , "other" ) ) . toBe ( path . join ( library , "other" ) ) ;
140+ for ( const [ root , name ] of [
141+ [ claude , "Deploy" ] ,
142+ [ codex , "deploy" ] ,
143+ [ claude , "linked-out" ] ,
144+ [ claude , "dup" ] ,
145+ [ codex , "dup" ] ,
146+ ] as const ) {
147+ expect ( yield * link ( root , name ) ) . toBeUndefined ( ) ;
148+ }
149+ expect ( yield * read ( claude , "linked-out" ) ) . toBe ( "shared" ) ;
150+ expect ( yield * read ( codex , ".dup.t3-discard" ) ) . toContain ( "mine" ) ;
125151 expect ( ( yield * service . status ) . conflicts ) . toEqual ( [
152+ { name : "Deploy" , paths : [ path . join ( claude , "Deploy" ) , path . join ( codex , "deploy" ) ] } ,
126153 { name : "review" , paths : [ path . join ( claude , "review" ) , path . join ( codex , "review" ) ] } ,
154+ {
155+ name : "linked-out" ,
156+ paths : [ path . join ( claude , "linked-out" ) , path . join ( claude , "linked-out" , "SKILL.md" ) ] ,
157+ reason : "outsideLink" ,
158+ } ,
159+ {
160+ name : "dup" ,
161+ paths : [ path . join ( codex , ".dup.t3-discard" ) , path . join ( claude , "dup" ) ] ,
162+ } ,
127163 ] ) ;
128164 } ) ,
129165 ) ;
@@ -151,11 +187,16 @@ it.layer(NodeServices.layer)("SkillsLibrary", (it) => {
151187 const { fs, path, home, claude, codex, library, service, skill, read, link } =
152188 yield * setup ( ) ;
153189 yield * skill ( claude , "hello" , "hello" ) ;
190+ yield * skill ( claude , "kept" , "kept" ) ;
154191 yield * skill ( codex , "codex-only" , "codex" ) ;
155192 yield * service . apply ;
156193 const elsewhere = path . join ( home , "dev" , "skills" ) ;
157194 yield * skill ( elsewhere , "mine" , "mine" ) ;
158195 yield * fs . symlink ( path . join ( elsewhere , "mine" ) , path . join ( claude , "mine" ) ) ;
196+ // Points into the library under another name, so it is the user's link.
197+ yield * fs . symlink ( path . join ( library , "hello" ) , path . join ( claude , "alias" ) ) ;
198+ // A folder at the staging path that is not a copy of what it would restore.
199+ yield * skill ( claude , ".kept.t3-restore" , "mine" ) ;
159200 // A crash after unlinking, before the rename, leaves only the staged copy.
160201 yield * fs . remove ( path . join ( claude , "codex-only" ) ) ;
161202 yield * fs . copy (
@@ -171,11 +212,62 @@ it.layer(NodeServices.layer)("SkillsLibrary", (it) => {
171212 expect ( yield * read ( claude , name ) ) . toBe ( yield * read ( library , name ) ) ;
172213 }
173214 expect ( yield * link ( claude , "mine" ) ) . toBe ( path . join ( elsewhere , "mine" ) ) ;
174- expect ( ( yield * fs . readDirectory ( claude ) ) . sort ( ) ) . toEqual ( [ "codex-only" , "hello" , "mine" ] ) ;
215+ expect ( yield * link ( claude , "alias" ) ) . toBe ( path . join ( library , "hello" ) ) ;
216+ expect ( yield * link ( claude , "kept" ) ) . toBe ( path . join ( library , "kept" ) ) ;
217+ expect ( yield * read ( claude , ".kept.t3-restore" ) ) . toContain ( "mine" ) ;
218+ expect ( ( yield * fs . readDirectory ( claude ) ) . sort ( ) ) . toEqual ( [
219+ ".kept.t3-restore" ,
220+ "alias" ,
221+ "codex-only" ,
222+ "hello" ,
223+ "kept" ,
224+ "mine" ,
225+ ] ) ;
175226 // Codex reads the library itself, so it gets no second copy.
176227 expect ( yield * fs . readDirectory ( codex ) ) . toEqual ( [ ] ) ;
177- expect ( ( yield * fs . readDirectory ( library ) ) . sort ( ) ) . toEqual ( [ "codex-only" , "hello" ] ) ;
178- expect ( ( yield * service . status ) . linked ) . toEqual ( [ ] ) ;
228+ expect ( ( yield * fs . readDirectory ( library ) ) . sort ( ) ) . toEqual ( [
229+ "codex-only" ,
230+ "hello" ,
231+ "kept" ,
232+ ] ) ;
233+ const status = yield * service . status ;
234+ expect ( status . linked ) . toEqual ( [ { driver : "claudeAgent" , name : "kept" } ] ) ;
235+ expect ( status . conflicts ) . toEqual ( [
236+ {
237+ name : "kept" ,
238+ paths : [ path . join ( claude , ".kept.t3-restore" ) , path . join ( claude , "kept" ) ] ,
239+ } ,
240+ ] ) ;
241+ } ) ,
242+ ) ;
243+
244+ it . effect . skipIf ( ! symlinksSupported ) (
245+ "finishes at startup a detach a restart interrupted while the switch is off" ,
246+ ( ) =>
247+ Effect . gen ( function * ( ) {
248+ const { fs, path, home, claude, library, service, skill, read, link } = yield * setup ( ) ;
249+ yield * skill ( claude , "hello" , "hello" ) ;
250+ yield * service . apply ;
251+ yield * fs . remove ( path . join ( claude , "hello" ) ) ;
252+ yield * fs . copy ( path . join ( library , "hello" ) , path . join ( claude , ".hello.t3-restore" ) ) ;
253+
254+ yield * Layer . build ( SkillsLibrary . layer ) . pipe (
255+ Effect . provide (
256+ Layer . mergeAll (
257+ ServerSettings . ServerSettingsService . layerTest ( ) ,
258+ Layer . mock ( ProviderInstanceRegistry . ProviderInstanceRegistry ) ( {
259+ listInstances : Effect . succeed ( [ ] ) ,
260+ subscribeChanges : PubSub . unbounded < void > ( ) . pipe ( Effect . flatMap ( PubSub . subscribe ) ) ,
261+ } ) ,
262+ ) ,
263+ ) ,
264+ Effect . provideService ( HostProcess . HomeDirectory , home ) ,
265+ Effect . provideService ( HostProcess . Environment , { HOME : home } ) ,
266+ ) ;
267+
268+ expect ( yield * link ( claude , "hello" ) ) . toBeUndefined ( ) ;
269+ expect ( yield * read ( claude , "hello" ) ) . toBe ( yield * read ( library , "hello" ) ) ;
270+ expect ( yield * fs . readDirectory ( claude ) ) . toEqual ( [ "hello" ] ) ;
179271 } ) ,
180272 ) ;
181273 } ) ;
0 commit comments