1+ import { describe , it , expect } from 'vitest' ;
2+ import { runCli } from '../src/index' ;
3+ import { mkdtemp , writeFile , mkdir } from 'node:fs/promises' ;
4+ import { tmpdir } from 'node:os' ;
5+ import { join } from 'node:path' ;
6+
7+ const runtime = { platform : 'darwin' } as const ;
8+ const mkTmp = async ( ) => mkdtemp ( join ( tmpdir ( ) , 'ccsdd-cli-edge-' ) ) ;
9+
10+ const makeIO = ( ) => {
11+ const logs : string [ ] = [ ] ;
12+ const errs : string [ ] = [ ] ;
13+ let exitCode : number | null = null ;
14+ return {
15+ io : {
16+ log : ( m : string ) => logs . push ( m ) ,
17+ error : ( m : string ) => errs . push ( m ) ,
18+ exit : ( c : number ) => {
19+ exitCode = c ;
20+ } ,
21+ } ,
22+ get logs ( ) { return logs ; } ,
23+ get errs ( ) { return errs ; } ,
24+ get exitCode ( ) { return exitCode ; }
25+ } ;
26+ } ;
27+
28+ describe ( 'CLI entry edge cases' , ( ) => {
29+ it ( 'handles multiple help flags' , async ( ) => {
30+ const ctx = makeIO ( ) ;
31+ const code = await runCli ( [ '--help' , '-h' ] , runtime , ctx . io , { } ) ;
32+ expect ( code ) . toBe ( 0 ) ;
33+ expect ( ctx . logs . join ( '\n' ) ) . toMatch ( / U s a g e : c c - s d d / ) ;
34+ } ) ;
35+
36+ it ( 'handles multiple version flags' , async ( ) => {
37+ const ctx = makeIO ( ) ;
38+ const code = await runCli ( [ '--version' , '-v' ] , runtime , ctx . io , { } ) ;
39+ expect ( code ) . toBe ( 0 ) ;
40+ expect ( ctx . logs . join ( '\n' ) ) . toMatch ( / c c - s d d v / ) ;
41+ } ) ;
42+
43+ it ( 'prioritizes help over version' , async ( ) => {
44+ const ctx = makeIO ( ) ;
45+ const code = await runCli ( [ '--version' , '--help' ] , runtime , ctx . io , { } ) ;
46+ expect ( code ) . toBe ( 0 ) ;
47+ expect ( ctx . logs . join ( '\n' ) ) . toMatch ( / U s a g e : c c - s d d / ) ;
48+ expect ( ctx . logs . join ( '\n' ) ) . not . toMatch ( / c c - s d d v / ) ;
49+ } ) ;
50+
51+ it ( 'handles missing manifest file in dry-run mode' , async ( ) => {
52+ const ctx = makeIO ( ) ;
53+ const nonExistentManifest = '/path/that/does/not/exist/manifest.json' ;
54+ const code = await runCli ( [ '--dry-run' , '--manifest' , nonExistentManifest ] , runtime , ctx . io , { } ) ;
55+ expect ( code ) . toBe ( 1 ) ;
56+ expect ( ctx . errs . join ( '\n' ) ) . toMatch ( / M a n i f e s t n o t f o u n d / ) ;
57+ } ) ;
58+
59+ it ( 'handles invalid manifest file in dry-run mode' , async ( ) => {
60+ const dir = await mkTmp ( ) ;
61+ const manifestPath = join ( dir , 'invalid.json' ) ;
62+ await writeFile ( manifestPath , '{ invalid json' , 'utf8' ) ;
63+
64+ const ctx = makeIO ( ) ;
65+ const code = await runCli ( [ '--dry-run' , '--manifest' , manifestPath ] , runtime , ctx . io , { } ) ;
66+ expect ( code ) . toBe ( 1 ) ;
67+ expect ( ctx . errs . join ( '\n' ) ) . toMatch ( / I n v a l i d J S O N / ) ;
68+ } ) ;
69+
70+ it ( 'handles missing manifest file in apply mode' , async ( ) => {
71+ const ctx = makeIO ( ) ;
72+ const nonExistentManifest = '/path/that/does/not/exist/manifest.json' ;
73+ const code = await runCli ( [ '--manifest' , nonExistentManifest ] , runtime , ctx . io , { } ) ;
74+ expect ( code ) . toBe ( 1 ) ;
75+ expect ( ctx . errs . join ( '\n' ) ) . toMatch ( / M a n i f e s t n o t f o u n d / ) ;
76+ } ) ;
77+
78+ it ( 'resolves default manifest path correctly' , async ( ) => {
79+ const templatesRoot = await mkTmp ( ) ;
80+ const manifestsDir = join ( templatesRoot , 'templates' , 'manifests' ) ;
81+ await mkdir ( manifestsDir , { recursive : true } ) ;
82+
83+ const manifestPath = join ( manifestsDir , 'claude-code.json' ) ;
84+ const manifest = {
85+ version : 1 ,
86+ artifacts : [
87+ {
88+ id : 'test' ,
89+ source : {
90+ type : 'templateFile' as const ,
91+ from : 'test.tpl.md' ,
92+ toDir : 'out'
93+ }
94+ }
95+ ]
96+ } ;
97+ await writeFile ( manifestPath , JSON . stringify ( manifest ) , 'utf8' ) ;
98+ await writeFile ( join ( templatesRoot , 'test.tpl.md' ) , '# Test {{AGENT}}' , 'utf8' ) ;
99+
100+ const ctx = makeIO ( ) ;
101+ const code = await runCli ( [ '--dry-run' ] , runtime , ctx . io , { } , { templatesRoot } ) ;
102+ expect ( code ) . toBe ( 0 ) ;
103+ expect ( ctx . logs . join ( '\n' ) ) . toMatch ( / P l a n \( d r y - r u n \) / ) ;
104+ } ) ;
105+
106+ it ( 'prefers minimal manifest when profile=minimal' , async ( ) => {
107+ const templatesRoot = await mkTmp ( ) ;
108+ const manifestsDir = join ( templatesRoot , 'templates' , 'manifests' ) ;
109+ await mkdir ( manifestsDir , { recursive : true } ) ;
110+
111+ const fullManifest = {
112+ version : 1 ,
113+ artifacts : [
114+ { id : 'full1' , source : { type : 'templateFile' as const , from : 'f1.tpl.md' , toDir : 'out' } } ,
115+ { id : 'full2' , source : { type : 'templateFile' as const , from : 'f2.tpl.md' , toDir : 'out' } }
116+ ]
117+ } ;
118+ const minimalManifest = {
119+ version : 1 ,
120+ artifacts : [
121+ { id : 'min1' , source : { type : 'templateFile' as const , from : 'm1.tpl.md' , toDir : 'out' } }
122+ ]
123+ } ;
124+
125+ await writeFile ( join ( manifestsDir , 'claude-code.json' ) , JSON . stringify ( fullManifest ) , 'utf8' ) ;
126+ await writeFile ( join ( manifestsDir , 'claude-code-min.json' ) , JSON . stringify ( minimalManifest ) , 'utf8' ) ;
127+
128+ const ctx = makeIO ( ) ;
129+ const code = await runCli ( [ '--dry-run' , '--profile' , 'minimal' ] , runtime , ctx . io , { } , { templatesRoot } ) ;
130+ expect ( code ) . toBe ( 0 ) ;
131+ const output = ctx . logs . join ( '\n' ) ;
132+ expect ( output ) . toMatch ( / m i n 1 / ) ;
133+ expect ( output ) . not . toMatch ( / f u l l 1 | f u l l 2 / ) ;
134+ } ) ;
135+
136+ it ( 'falls back to default manifest when minimal not found' , async ( ) => {
137+ const templatesRoot = await mkTmp ( ) ;
138+ const manifestsDir = join ( templatesRoot , 'templates' , 'manifests' ) ;
139+ await mkdir ( manifestsDir , { recursive : true } ) ;
140+
141+ const fullManifest = {
142+ version : 1 ,
143+ artifacts : [
144+ { id : 'full1' , source : { type : 'templateFile' as const , from : 'f1.tpl.md' , toDir : 'out' } }
145+ ]
146+ } ;
147+
148+ await writeFile ( join ( manifestsDir , 'claude-code.json' ) , JSON . stringify ( fullManifest ) , 'utf8' ) ;
149+ // No claude-code-min.json created
150+
151+ const ctx = makeIO ( ) ;
152+ const code = await runCli ( [ '--dry-run' , '--profile' , 'minimal' ] , runtime , ctx . io , { } , { templatesRoot } ) ;
153+ expect ( code ) . toBe ( 0 ) ;
154+ const output = ctx . logs . join ( '\n' ) ;
155+ expect ( output ) . toMatch ( / f u l l 1 / ) ;
156+ } ) ;
157+
158+ it ( 'handles empty argv array' , async ( ) => {
159+ const ctx = makeIO ( ) ;
160+ const code = await runCli ( [ ] , runtime , ctx . io , { } ) ;
161+ expect ( code ) . toBe ( 0 ) ; // Actually succeeds if it can load default manifest
162+ // Will try to apply default manifest - may succeed or fail depending on templates
163+ } ) ;
164+
165+ it ( 'handles execution error in apply mode' , async ( ) => {
166+ const templatesRoot = await mkTmp ( ) ;
167+ const manifestPath = join ( templatesRoot , 'manifest.json' ) ;
168+ const manifest = {
169+ version : 1 ,
170+ artifacts : [
171+ {
172+ id : 'test' ,
173+ source : {
174+ type : 'templateFile' as const ,
175+ from : 'nonexistent.tpl.md' , // This file doesn't exist
176+ toDir : 'out'
177+ }
178+ }
179+ ]
180+ } ;
181+ await writeFile ( manifestPath , JSON . stringify ( manifest ) , 'utf8' ) ;
182+
183+ const ctx = makeIO ( ) ;
184+ const code = await runCli ( [ '--manifest' , manifestPath ] , runtime , ctx . io , { } , { templatesRoot } ) ;
185+ expect ( code ) . toBe ( 1 ) ;
186+ expect ( ctx . errs . join ( '\n' ) ) . toMatch ( / E r r o r : / ) ;
187+ } ) ;
188+ } ) ;
0 commit comments