@@ -2518,6 +2518,98 @@ describe("pipeline install command", () => {
25182518 ) ;
25192519 } ) ;
25202520
2521+ it ( "cleans a partial steps.yaml when initial scaffold creation fails" , async ( ) => {
2522+ const stepsPath = "/repo/.poe-code/pipeline/steps.yaml" ;
2523+ const fs = createMemFs ( ) ;
2524+ const originalWriteFile = fs . writeFile . bind ( fs ) ;
2525+ vi . spyOn ( fs , "writeFile" ) . mockImplementation ( async ( filePath , data , options ) => {
2526+ if ( String ( filePath ) === stepsPath ) {
2527+ await originalWriteFile ( filePath , "partial steps\n" , options ) ;
2528+ throw new Error ( "injected partial steps write failure" ) ;
2529+ }
2530+ await originalWriteFile ( filePath , data , options ) ;
2531+ } ) ;
2532+ const container = createCliContainer ( {
2533+ fs,
2534+ prompts : vi . fn ( ) . mockResolvedValue ( { } ) ,
2535+ env : { cwd, homeDir } ,
2536+ logger : ( ) => { }
2537+ } ) ;
2538+ const program = createBaseProgram ( ) ;
2539+ registerPipelineCommand ( program , container ) ;
2540+
2541+ await expect (
2542+ program . parseAsync ( [
2543+ "node" ,
2544+ "cli" ,
2545+ "pipeline" ,
2546+ "install" ,
2547+ "--agent" ,
2548+ "claude-code" ,
2549+ "--local"
2550+ ] )
2551+ ) . rejects . toThrow ( "injected partial steps write failure" ) ;
2552+
2553+ await expect ( fs . readFile ( stepsPath , "utf8" ) ) . rejects . toMatchObject ( { code : "ENOENT" } ) ;
2554+ await expect (
2555+ fs . readFile ( "/repo/.claude/skills/poe-code-pipeline-plan/SKILL.md" , "utf8" )
2556+ ) . rejects . toThrow ( ) ;
2557+ await expect ( fs . stat ( "/repo/.poe-code/pipeline/plans" ) ) . rejects . toThrow ( ) ;
2558+ } ) ;
2559+
2560+ it ( "cleans a partial forced steps temp file and restores the prior steps.yaml" , async ( ) => {
2561+ const stepsPath = "/repo/.poe-code/pipeline/steps.yaml" ;
2562+ const fs = createMemFs ( {
2563+ [ stepsPath ] : "EXISTING_STEPS"
2564+ } ) ;
2565+ const originalWriteFile = fs . writeFile . bind ( fs ) ;
2566+ let temporaryPath : string | undefined ;
2567+ vi . spyOn ( fs , "writeFile" ) . mockImplementation ( async ( filePath , data , options ) => {
2568+ const filePathText = String ( filePath ) ;
2569+ if (
2570+ temporaryPath === undefined &&
2571+ filePathText . startsWith ( `${ stepsPath } .${ process . pid } .` ) &&
2572+ filePathText . endsWith ( ".tmp" )
2573+ ) {
2574+ temporaryPath = filePathText ;
2575+ await originalWriteFile ( filePath , "partial forced steps\n" , options ) ;
2576+ throw new Error ( "injected forced steps write failure" ) ;
2577+ }
2578+ await originalWriteFile ( filePath , data , options ) ;
2579+ } ) ;
2580+ const container = createCliContainer ( {
2581+ fs,
2582+ prompts : vi . fn ( ) . mockResolvedValue ( { } ) ,
2583+ env : { cwd, homeDir } ,
2584+ logger : ( ) => { }
2585+ } ) ;
2586+ const program = createBaseProgram ( ) ;
2587+ registerPipelineCommand ( program , container ) ;
2588+
2589+ await expect (
2590+ program . parseAsync ( [
2591+ "node" ,
2592+ "cli" ,
2593+ "pipeline" ,
2594+ "install" ,
2595+ "--agent" ,
2596+ "claude-code" ,
2597+ "--local" ,
2598+ "--force"
2599+ ] )
2600+ ) . rejects . toThrow ( "injected forced steps write failure" ) ;
2601+
2602+ expect ( temporaryPath ) . toBeDefined ( ) ;
2603+ await expect ( fs . readFile ( stepsPath , "utf8" ) ) . resolves . toBe ( "EXISTING_STEPS" ) ;
2604+ await expect ( fs . readFile ( temporaryPath as string , "utf8" ) ) . rejects . toMatchObject ( {
2605+ code : "ENOENT"
2606+ } ) ;
2607+ await expect (
2608+ fs . readFile ( "/repo/.claude/skills/poe-code-pipeline-plan/SKILL.md" , "utf8" )
2609+ ) . rejects . toThrow ( ) ;
2610+ await expect ( fs . stat ( "/repo/.poe-code/pipeline/plans" ) ) . rejects . toThrow ( ) ;
2611+ } ) ;
2612+
25212613 it ( "does not install the skill or plans directory when steps creation fails" , async ( ) => {
25222614 const fs = createMemFs ( ) ;
25232615 const container = createCliContainer ( {
0 commit comments