1- import { existsSync , mkdtempSync } from "node:fs" ;
1+ import { existsSync , mkdirSync , mkdtempSync , readFileSync , writeFileSync } from "node:fs" ;
22import { tmpdir } from "node:os" ;
33import { join } from "node:path" ;
44import { describe , expect , test } from "vite-plus/test" ;
@@ -13,6 +13,31 @@ function makeTempConfigDir(): string {
1313 return mkdtempSync ( join ( tmpdir ( ) , "bl-skill-e2e-" ) ) ;
1414}
1515
16+ /** 在隔离 configDir 里种一个假 skill + lock,供 remove/update --dry-run 本地断言 */
17+ function seedInstalledSkill ( configDir : string , skillName : string , links : string [ ] ) : void {
18+ const skillDir = join ( configDir , "skills" , skillName ) ;
19+ mkdirSync ( skillDir , { recursive : true } ) ;
20+ writeFileSync ( join ( skillDir , "SKILL.md" ) , "---\nname: seeded\n---\n" ) ;
21+ writeFileSync (
22+ join ( configDir , "skills" , "skill-lock.json" ) ,
23+ `${ JSON . stringify (
24+ {
25+ version : 1 ,
26+ skills : {
27+ [ skillName ] : {
28+ contentHash : "sha256:seeded-for-dry-run" ,
29+ installedAt : new Date ( ) . toISOString ( ) ,
30+ sourceType : "oss" ,
31+ links,
32+ } ,
33+ } ,
34+ } ,
35+ null ,
36+ 2 ,
37+ ) } \n`,
38+ ) ;
39+ }
40+
1641describe ( "e2e: skill" , ( ) => {
1742 test ( "skill add --help exits successfully" , async ( ) => {
1843 const { stderr, exitCode } = await runCommandHelp ( SKILL_ROUTES , [ "skill" , "add" , "--help" ] ) ;
@@ -47,7 +72,7 @@ describe("e2e: skill", () => {
4772 } ) ;
4873} ) ;
4974
50- // Local-only cases: auth "none" + validation happens before any network access, no gating needed
75+ // Local-only cases: auth "none" + dry-run / validation happen before any network access
5176describe ( "e2e: skill (local, no credentials)" , ( ) => {
5277 test ( "skill add without --all or --name errors as usage error (2)" , async ( ) => {
5378 const { stdout, stderr, exitCode } = await runCommandE2e ( SKILL_ROUTES , [
@@ -96,6 +121,102 @@ describe("e2e: skill (local, no credentials)", () => {
96121 expect ( data . skills ?. [ 0 ] ?. status ) . toBe ( "failed" ) ;
97122 expect ( data . skills ?. [ 0 ] ?. reason ) . toMatch ( / n o t i n s t a l l e d / i) ;
98123 } ) ;
124+
125+ test ( "skill remove --dry-run 仅输出计划且不删盘" , async ( ) => {
126+ const configDir = makeTempConfigDir ( ) ;
127+ const fakeHome = makeTempConfigDir ( ) ;
128+ const linkPath = join ( fakeHome , ".claude" , "skills" , "seeded-skill" ) ;
129+ seedInstalledSkill ( configDir , "seeded-skill" , [ linkPath ] ) ;
130+ mkdirSync ( join ( fakeHome , ".claude" , "skills" ) , { recursive : true } ) ;
131+ writeFileSync ( linkPath , "link-placeholder" ) ;
132+
133+ const { stdout, stderr, exitCode } = await runCommandE2e (
134+ SKILL_ROUTES ,
135+ [ "skill" , "remove" , "--name" , "seeded-skill" , "--dry-run" , "--output" , "json" ] ,
136+ { BAILIAN_CONFIG_DIR : configDir , HOME : fakeHome , USERPROFILE : fakeHome } ,
137+ ) ;
138+ expect ( exitCode , stderr ) . toBe ( 0 ) ;
139+ const data = parseStdoutJson < {
140+ action ?: string ;
141+ skills ?: Array < { name ?: string ; removedLinks ?: number } > ;
142+ } > ( stdout ) ;
143+ expect ( data . action ) . toBe ( "skill.remove" ) ;
144+ expect ( data . skills ?. [ 0 ] ?. name ) . toBe ( "seeded-skill" ) ;
145+ expect ( data . skills ?. [ 0 ] ?. removedLinks ) . toBe ( 1 ) ;
146+ expect ( existsSync ( join ( configDir , "skills" , "seeded-skill" , "SKILL.md" ) ) ) . toBe ( true ) ;
147+ expect ( existsSync ( join ( configDir , "skills" , "skill-lock.json" ) ) ) . toBe ( true ) ;
148+ expect ( existsSync ( linkPath ) ) . toBe ( true ) ;
149+ } ) ;
150+
151+ test ( "skill add --dry-run 仅输出计划且不写盘、不联网" , async ( ) => {
152+ const configDir = makeTempConfigDir ( ) ;
153+ const fakeHome = makeTempConfigDir ( ) ;
154+ const { stdout, stderr, exitCode } = await runCommandE2e (
155+ SKILL_ROUTES ,
156+ [ "skill" , "add" , "--name" , WIKI_SKILL , "--dry-run" , "--output" , "json" ] ,
157+ { BAILIAN_CONFIG_DIR : configDir , HOME : fakeHome , USERPROFILE : fakeHome } ,
158+ ) ;
159+ expect ( exitCode , stderr ) . toBe ( 0 ) ;
160+ const data = parseStdoutJson < {
161+ action ?: string ;
162+ skills ?: string [ ] | string ;
163+ registry ?: string ;
164+ } > ( stdout ) ;
165+ expect ( data . action ) . toBe ( "skill.add" ) ;
166+ expect ( data . registry ) . toMatch ( / ^ h t t p s ? : \/ \/ / ) ;
167+ expect ( data . skills ) . toEqual ( [ WIKI_SKILL ] ) ;
168+ expect ( existsSync ( join ( configDir , "skills" , WIKI_SKILL ) ) ) . toBe ( false ) ;
169+ expect ( existsSync ( join ( configDir , "skills" , "skill-lock.json" ) ) ) . toBe ( false ) ;
170+ } ) ;
171+
172+ test ( "skill init --dry-run 仅输出筛选意图且不写盘、不联网" , async ( ) => {
173+ const configDir = makeTempConfigDir ( ) ;
174+ const fakeHome = makeTempConfigDir ( ) ;
175+ const { stdout, stderr, exitCode } = await runCommandE2e (
176+ SKILL_ROUTES ,
177+ [ "skill" , "init" , "--dry-run" , "--output" , "json" ] ,
178+ { BAILIAN_CONFIG_DIR : configDir , HOME : fakeHome , USERPROFILE : fakeHome } ,
179+ ) ;
180+ expect ( exitCode , stderr ) . toBe ( 0 ) ;
181+ const data = parseStdoutJson < {
182+ action ?: string ;
183+ skills ?: string ;
184+ registry ?: string ;
185+ } > ( stdout ) ;
186+ expect ( data . action ) . toBe ( "skill.init" ) ;
187+ expect ( data . registry ) . toMatch ( / ^ h t t p s ? : \/ \/ / ) ;
188+ expect ( data . skills ) . toBe ( "bailian-*" ) ;
189+ expect ( existsSync ( join ( configDir , "skills" ) ) ) . toBe ( false ) ;
190+ } ) ;
191+
192+ test ( "skill update --dry-run 仅输出计划且不 fan-out、不写 lock" , async ( ) => {
193+ const configDir = makeTempConfigDir ( ) ;
194+ const fakeHome = makeTempConfigDir ( ) ;
195+ const linkPath = join ( fakeHome , ".claude" , "skills" , "seeded-skill" ) ;
196+ seedInstalledSkill ( configDir , "seeded-skill" , [ linkPath ] ) ;
197+ mkdirSync ( join ( fakeHome , ".claude" , "skills" ) , { recursive : true } ) ;
198+ writeFileSync ( linkPath , "link-placeholder" ) ;
199+ const lockBefore = readFileSync ( join ( configDir , "skills" , "skill-lock.json" ) , "utf-8" ) ;
200+
201+ const { stdout, stderr, exitCode } = await runCommandE2e (
202+ SKILL_ROUTES ,
203+ [ "skill" , "update" , "--name" , "seeded-skill" , "--dry-run" , "--output" , "json" ] ,
204+ { BAILIAN_CONFIG_DIR : configDir , HOME : fakeHome , USERPROFILE : fakeHome } ,
205+ ) ;
206+ expect ( exitCode , stderr ) . toBe ( 0 ) ;
207+ const data = parseStdoutJson < {
208+ action ?: string ;
209+ skills ?: string [ ] ;
210+ registry ?: string ;
211+ } > ( stdout ) ;
212+ expect ( data . action ) . toBe ( "skill.update" ) ;
213+ expect ( data . registry ) . toMatch ( / ^ h t t p s ? : \/ \/ / ) ;
214+ expect ( data . skills ) . toEqual ( [ "seeded-skill" ] ) ;
215+ expect ( readFileSync ( join ( configDir , "skills" , "skill-lock.json" ) , "utf-8" ) ) . toBe ( lockBefore ) ;
216+ expect ( existsSync ( join ( configDir , "skills" , "seeded-skill" , "SKILL.md" ) ) ) . toBe ( true ) ;
217+ expect ( existsSync ( linkPath ) ) . toBe ( true ) ;
218+ expect ( readFileSync ( linkPath , "utf-8" ) ) . toBe ( "link-placeholder" ) ;
219+ } ) ;
99220} ) ;
100221
101222describe . skipIf ( ! isBailianE2EEnabled ( ) ) ( "e2e: skill (real registry)" , ( ) => {
0 commit comments