@@ -620,6 +620,86 @@ func TestRunBlocksCompletedEvidenceBeforeFinishGate(t *testing.T) {
620620 }
621621}
622622
623+ func TestRepairDoesNotBypassFailedFinishGate (t * testing.T ) {
624+ root := t .TempDir ()
625+ mustInitWithPlan (t , root , "Tiny notes" , "Build a tiny notes MVP" )
626+ mustRun (t , root , "run" )
627+ writeFile (t , filepath .Join (root , ".hyper" , "goals" , "GOAL-0001" , "evidence.md" ), "# GOAL-0001 Evidence\n \n ## Validation\n \n `go test ./...` passed.\n \n ## Readiness Evidence\n \n Core UX: flow exists.\n Validation coverage: `go test ./...` passed and is repeatable.\n \n ## Blocker\n \n None blocking.\n " )
628+ writeFile (t , filepath .Join (root , ".hyper" , "goals" , "GOAL-0001" , "next.md" ), "# GOAL-0001 Next\n \n ## Recommended Next Goal\n \n Start another packet.\n " )
629+
630+ if _ , err := runCLI (args ("complete" ), testRoot (root ), fakeUpdater {}); err == nil {
631+ t .Fatal ("expected finish gate failure" )
632+ }
633+ review := readFile (t , filepath .Join (root , ".hyper" , "goals" , "GOAL-0001" , "review.md" ))
634+ assertContains (t , review , "Status: failed" )
635+ if status := finishGateReviewStatus (root , "GOAL-0001" ); status != "failed" {
636+ t .Fatalf ("expected failed finish gate review status, got %q" , status )
637+ }
638+ if _ , ok := failedFinishGateGoalState (root , "GOAL-0001" ); ! ok {
639+ t .Fatal ("expected failed finish gate state to be visible" )
640+ }
641+
642+ status , err := runCLI (args ("status" , "--short" ), testRoot (root ), fakeUpdater {})
643+ if err != nil {
644+ t .Fatalf ("status failed: %v" , err )
645+ }
646+ assertContains (t , status .Stdout , "Finish gate failed" )
647+ assertNotContains (t , status .Stdout , "Next: hyper repair" )
648+
649+ repair , err := runCLI (args ("repair" ), testRoot (root ), fakeUpdater {})
650+ if err != nil {
651+ t .Fatalf ("repair failed: %v" , err )
652+ }
653+ assertContains (t , repair .Stdout , "State: no repair needed" )
654+ assertContains (t , repair .Stdout , "Finish gate failed" )
655+ state , hyperErr := readState (filepath .Join (root , ".hyper" , "state.json" ))
656+ if hyperErr != nil {
657+ t .Fatal (hyperErr )
658+ }
659+ if state .Status != "active" {
660+ t .Fatalf ("repair must not mark failed finish gate completed, got %s" , state .Status )
661+ }
662+
663+ _ , err = runCLI (args ("run" , "Start another packet" ), testRoot (root ), fakeUpdater {})
664+ if err == nil {
665+ t .Fatal ("expected failed finish gate to block another run" )
666+ }
667+ assertContains (t , err .Message , "failed the finish gate" )
668+
669+ state .Status = "completed"
670+ if err := writeJSON (filepath .Join (root , ".hyper" , "state.json" ), state ); err != nil {
671+ t .Fatal (err )
672+ }
673+ status , err = runCLI (args ("status" , "--short" ), testRoot (root ), fakeUpdater {})
674+ if err != nil {
675+ t .Fatalf ("status failed after legacy state write: %v" , err )
676+ }
677+ assertContains (t , status .Stdout , "Finish gate failed" )
678+ assertNotContains (t , status .Stdout , "Next: hyper repair" )
679+ repair , err = runCLI (args ("repair" ), testRoot (root ), fakeUpdater {})
680+ if err != nil {
681+ t .Fatalf ("legacy repair failed: %v" , err )
682+ }
683+ assertContains (t , repair .Stdout , "State: repaired" )
684+ assertContains (t , repair .Stdout , "To: active" )
685+ assertContains (t , repair .Stdout , "Next action: hyper complete" )
686+ nextPacket := readFile (t , filepath .Join (root , ".hyper" , "next-packet.md" ))
687+ assertContains (t , nextPacket , "Action: complete-current" )
688+ assertContains (t , nextPacket , "Command: hyper complete" )
689+ state , hyperErr = readState (filepath .Join (root , ".hyper" , "state.json" ))
690+ if hyperErr != nil {
691+ t .Fatal (hyperErr )
692+ }
693+ if state .Status != "active" {
694+ t .Fatalf ("legacy failed finish gate repair must restore active state, got %s" , state .Status )
695+ }
696+ _ , err = runCLI (args ("run" , "Start another packet" ), testRoot (root ), fakeUpdater {})
697+ if err == nil {
698+ t .Fatal ("expected failed finish gate to block another run even when state was marked completed" )
699+ }
700+ assertContains (t , err .Message , "failed the finish gate" )
701+ }
702+
623703func TestCompleteLearnsAndRefreshesReadiness (t * testing.T ) {
624704 root := t .TempDir ()
625705 mustInitWithPlan (t , root , "Tiny notes" , "Build a tiny notes MVP" )
0 commit comments