1- import { mkdtempSync , writeFileSync } from "node:fs" ;
1+ import { mkdtempSync , rmSync , writeFileSync } from "node:fs" ;
22import { tmpdir } from "node:os" ;
33import { join } from "node:path" ;
44import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
55import { runCommand , type CommandDef } from "citty" ;
66import { CliUsageError } from "./commandResult.js" ;
7+ import { resolveExtraPositionals } from "./reject-extra-positionals.js" ;
78
89const trackCommandFailure = vi . fn ( ) ;
910vi . mock ( "../telemetry/events.js" , ( ) => ( {
@@ -140,6 +141,12 @@ describe("trackCommandFailures: extra positionals", () => {
140141 const wrap = ( cmd : CommandDef ) => trackCommandFailures ( ( ) => Promise . resolve ( cmd ) ) ( ) ;
141142 let errorSpy : ReturnType < typeof vi . spyOn > ;
142143 let logSpy : ReturnType < typeof vi . spyOn > ;
144+ const tempDirs : string [ ] = [ ] ;
145+ const tempDir = ( ) => {
146+ const dir = mkdtempSync ( join ( tmpdir ( ) , "hf-join-" ) ) ;
147+ tempDirs . push ( dir ) ;
148+ return dir ;
149+ } ;
143150
144151 beforeEach ( ( ) => {
145152 errorSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
@@ -149,6 +156,45 @@ describe("trackCommandFailures: extra positionals", () => {
149156 afterEach ( ( ) => {
150157 errorSpy . mockRestore ( ) ;
151158 logSpy . mockRestore ( ) ;
159+ for ( const dir of tempDirs . splice ( 0 ) ) rmSync ( dir , { recursive : true , force : true } ) ;
160+ } ) ;
161+
162+ it ( "throws a presented error whose message carries a count, never the arguments" , async ( ) => {
163+ const error = await runCommand ( await wrap ( leaf ( "render" ) ) , {
164+ rawArgs : [ "./proj" , "secret-launch" , "Jane" ] ,
165+ } ) . catch ( ( e : unknown ) => e ) ;
166+ expect ( error ) . toBeInstanceOf ( CliUsageError ) ;
167+ expect ( ( error as CliUsageError ) . result . presented ) . toBe ( true ) ;
168+ expect ( ( error as Error ) . message ) . toBe ( "2 unexpected extra arguments for hyperframes render" ) ;
169+ } ) ;
170+
171+ it ( "labels a required positional <NAME> and an optional one [NAME]" , async ( ) => {
172+ const cmd = {
173+ meta : { name : "keyframes" } ,
174+ args : { file : { type : "positional" } , at : { type : "positional" , required : false } } ,
175+ run : vi . fn ( ) ,
176+ } ;
177+ await expect (
178+ runCommand ( await wrap ( cmd as CommandDef ) , { rawArgs : [ "a" , "b" , "c" ] } ) ,
179+ ) . rejects . toThrow ( CliUsageError ) ;
180+ expect ( errorSpy . mock . calls . flat ( ) . join ( "\n" ) ) . toContain (
181+ "Usage: hyperframes keyframes <FILE> [AT]" ,
182+ ) ;
183+ } ) ;
184+
185+ it . each ( [
186+ [ "figma asset" , ( ) => import ( "../commands/figma/asset.js" ) ] ,
187+ [
188+ "skills update" ,
189+ async ( ) => ( {
190+ default : ( await import ( "../commands/skills.js" ) ) . default . subCommands ! . update ,
191+ } ) ,
192+ ] ,
193+ ] ) ( "lets the real %s command read several positionals" , async ( path , load ) => {
194+ const def = ( await ( load as ( ) => Promise < { default : unknown } > ) ( ) ) . default ;
195+ const cmd = ( typeof def === "function" ? await def ( ) : def ) as CommandDef ;
196+ const parsed : Record < string , unknown > = { _ : [ "one" , "two" , "three" ] } ;
197+ expect ( ( ) => resolveExtraPositionals ( cmd , path as string , parsed , [ ] ) ) . not . toThrow ( ) ;
152198 } ) ;
153199
154200 it ( "rejects a leaf's extra positional, naming it and the usage line, before run()" , async ( ) => {
@@ -178,17 +224,18 @@ describe("trackCommandFailures: extra positionals", () => {
178224
179225 it ( "rejects instead of joining when the first word names a file" , async ( ) => {
180226 const run = vi . fn ( ) ;
181- const file = join ( mkdtempSync ( join ( tmpdir ( ) , "hf-join-" ) ) , "script.txt" ) ;
227+ const file = join ( tempDir ( ) , "script.txt" ) ;
182228 writeFileSync ( file , "hello" ) ;
183229 await expect (
184230 runCommand ( await wrap ( leaf ( "tts" , run ) ) , { rawArgs : [ file , "extra" ] } ) ,
185- ) . rejects . toThrow ( / e x t r a a r g u m e n t f o r h y p e r f r a m e s t t s : e x t r a / ) ;
231+ ) . rejects . toThrow ( CliUsageError ) ;
232+ expect ( errorSpy . mock . calls . flat ( ) . join ( "\n" ) ) . toContain ( "hyperframes tts: extra" ) ;
186233 expect ( run ) . not . toHaveBeenCalled ( ) ;
187234 } ) ;
188235
189236 it ( "still joins catalog words when the first one names a folder" , async ( ) => {
190237 const run = vi . fn ( ) ;
191- const folder = mkdtempSync ( join ( tmpdir ( ) , "hf-join-" ) ) ;
238+ const folder = tempDir ( ) ;
192239 await runCommand ( await wrap ( leaf ( "catalog" , run ) ) , { rawArgs : [ folder , "player" ] } ) ;
193240 expect ( run . mock . calls [ 0 ] ! [ 0 ] . args . dir ) . toBe ( `${ folder } player` ) ;
194241 } ) ;
@@ -227,7 +274,10 @@ describe("trackCommandFailures: extra positionals", () => {
227274 run : vi . fn ( ) ,
228275 } as CommandDef ;
229276 await expect ( runCommand ( await wrap ( group ) , { rawArgs : [ "get" , "id1" , "id2" ] } ) ) . rejects . toThrow (
230- / h y p e r f r a m e s c l o u d g e t : i d 2 \n U s a g e : h y p e r f r a m e s c l o u d g e t \[ D I R \] / ,
277+ CliUsageError ,
278+ ) ;
279+ expect ( errorSpy . mock . calls . flat ( ) . join ( "\n" ) ) . toContain (
280+ "hyperframes cloud get: id2\nUsage: hyperframes cloud get [DIR]" ,
231281 ) ;
232282 } ) ;
233283
0 commit comments