11import { afterEach , describe , expect , test } from "bun:test" ;
2- import { mkdtempSync , rmSync } from "node:fs" ;
3- import { readFile } from "node:fs/promises" ;
2+ import { mkdirSync , mkdtempSync , rmSync } from "node:fs" ;
3+ import { readFile , writeFile } from "node:fs/promises" ;
44import { tmpdir } from "node:os" ;
55import { join } from "node:path" ;
66import { createSession , deleteSession , loadSession , saveSession } from "../src/runtime/session" ;
7- import { getFeatureDocPath , getIndexDocPath } from "../src/runtime/paths" ;
8- import { summarizeSession } from "../src/runtime/summary" ;
7+ import { getFeatureDocPath , getIndexDocPath , getSessionPath } from "../src/runtime/paths" ;
8+ import { deriveNextCommand , summarizeSession } from "../src/runtime/summary" ;
99import { createTools } from "../src/tools" ;
1010import { approvePlan , applyPlan , completeRun , recordReviewerDecision , resetFeature , selectPlanFeatures , startRun } from "../src/runtime/transitions" ;
1111
@@ -63,6 +63,27 @@ describe("runtime transitions", () => {
6363 expect ( indexDoc ) . toContain ( "goal: Build a workflow plugin" ) ;
6464 } ) ;
6565
66+ test ( "rejects malformed persisted session data" , async ( ) => {
67+ const worktree = makeTempDir ( ) ;
68+ mkdirSync ( join ( worktree , ".flow" ) , { recursive : true } ) ;
69+ await writeFile ( getSessionPath ( worktree ) , "{not valid json" , "utf8" ) ;
70+
71+ await expect ( loadSession ( worktree ) ) . rejects . toThrow ( ) ;
72+ } ) ;
73+
74+ test ( "saveSession refreshes updatedAt while preserving createdAt" , async ( ) => {
75+ const worktree = makeTempDir ( ) ;
76+ const created = createSession ( "Build a workflow plugin" ) ;
77+ const firstSave = await saveSession ( worktree , created ) ;
78+
79+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
80+
81+ const secondSave = await saveSession ( worktree , firstSave ) ;
82+
83+ expect ( secondSave . timestamps . createdAt ) . toBe ( firstSave . timestamps . createdAt ) ;
84+ expect ( new Date ( secondSave . timestamps . updatedAt ) . getTime ( ) ) . toBeGreaterThan ( new Date ( firstSave . timestamps . updatedAt ) . getTime ( ) ) ;
85+ } ) ;
86+
6687 test ( "renders feature docs for planned work" , async ( ) => {
6788 const worktree = makeTempDir ( ) ;
6889 const session = createSession ( "Build a workflow plugin" ) ;
@@ -78,6 +99,26 @@ describe("runtime transitions", () => {
7899 expect ( featureDoc ) . toContain ( "src/runtime/session.ts" ) ;
79100 } ) ;
80101
102+ test ( "prunes stale feature docs when a plan is narrowed" , async ( ) => {
103+ const worktree = makeTempDir ( ) ;
104+ const session = createSession ( "Build a workflow plugin" ) ;
105+ const applied = applyPlan ( session , samplePlan ( ) ) ;
106+ expect ( applied . ok ) . toBe ( true ) ;
107+ if ( ! applied . ok ) return ;
108+
109+ await saveSession ( worktree , applied . value ) ;
110+ await expect ( readFile ( getFeatureDocPath ( worktree , "execute-feature" ) , "utf8" ) ) . resolves . toContain ( "# Feature execute-feature" ) ;
111+
112+ const selected = selectPlanFeatures ( applied . value , [ "setup-runtime" ] ) ;
113+ expect ( selected . ok ) . toBe ( true ) ;
114+ if ( ! selected . ok ) return ;
115+
116+ await saveSession ( worktree , selected . value ) ;
117+
118+ await expect ( readFile ( getFeatureDocPath ( worktree , "setup-runtime" ) , "utf8" ) ) . resolves . toContain ( "# Feature setup-runtime" ) ;
119+ await expect ( readFile ( getFeatureDocPath ( worktree , "execute-feature" ) , "utf8" ) ) . rejects . toThrow ( ) ;
120+ } ) ;
121+
81122 test ( "renders multiline content without breaking markdown structure" , async ( ) => {
82123 const worktree = makeTempDir ( ) ;
83124 const session = createSession ( "Build a workflow plugin\nwith multiline context" ) ;
@@ -721,6 +762,55 @@ describe("runtime transitions", () => {
721762 expect ( indexDoc ) . toContain ( "next step: none" ) ;
722763 } ) ;
723764
765+ test ( "summarizeSession reports missing state when no session exists" , ( ) => {
766+ expect ( summarizeSession ( null ) ) . toEqual ( {
767+ status : "missing" ,
768+ summary : "No active Flow session found." ,
769+ } ) ;
770+ } ) ;
771+
772+ test ( "deriveNextCommand covers planning, runnable, blocked-human, and completed branches" , ( ) => {
773+ const planning = createSession ( "Build a workflow plugin" ) ;
774+ expect ( deriveNextCommand ( planning ) ) . toBe ( "/flow-plan <goal>" ) ;
775+
776+ const applied = applyPlan ( planning , samplePlan ( ) ) ;
777+ expect ( applied . ok ) . toBe ( true ) ;
778+ if ( ! applied . ok ) return ;
779+
780+ expect ( deriveNextCommand ( applied . value ) ) . toBe ( "/flow-plan" ) ;
781+
782+ const approved = approvePlan ( applied . value ) ;
783+ expect ( approved . ok ) . toBe ( true ) ;
784+ if ( ! approved . ok ) return ;
785+
786+ expect ( deriveNextCommand ( approved . value ) ) . toBe ( "/flow-run" ) ;
787+
788+ const running = startRun ( approved . value ) ;
789+ expect ( running . ok ) . toBe ( true ) ;
790+ if ( ! running . ok ) return ;
791+
792+ expect ( deriveNextCommand ( running . value . session ) ) . toBe ( "/flow-run" ) ;
793+
794+ const blocked = {
795+ ...approved . value ,
796+ status : "blocked" as const ,
797+ execution : {
798+ ...approved . value . execution ,
799+ lastFeatureId : "setup-runtime" ,
800+ lastOutcome : {
801+ kind : "blocked_external" as const ,
802+ summary : "Waiting on human decision." ,
803+ needsHuman : true ,
804+ } ,
805+ } ,
806+ } ;
807+
808+ expect ( deriveNextCommand ( blocked ) ) . toBe ( "/flow-status" ) ;
809+
810+ const completed = { ...approved . value , status : "completed" as const } ;
811+ expect ( deriveNextCommand ( completed ) ) . toBe ( "/flow-plan <goal>" ) ;
812+ } ) ;
813+
724814 test ( "suggests resetting blocked features when the outcome is retryable" , ( ) => {
725815 const session = createSession ( "Build a workflow plugin" ) ;
726816 const applied = applyPlan ( session , samplePlan ( ) ) ;
@@ -1200,6 +1290,54 @@ describe("runtime transitions", () => {
12001290 expect ( parsed . session . lastOutcomeKind ) . toBe ( "completed" ) ;
12011291 } ) ;
12021292
1293+ test ( "flow_status returns a machine-readable missing-session summary" , async ( ) => {
1294+ const worktree = makeTempDir ( ) ;
1295+ const tools = createTools ( { } ) as any ;
1296+ const response = await tools . flow_status . execute ( { } , { worktree } ) ;
1297+ const parsed = JSON . parse ( response ) ;
1298+
1299+ expect ( parsed . status ) . toBe ( "missing" ) ;
1300+ expect ( parsed . summary ) . toBe ( "No active Flow session found." ) ;
1301+ } ) ;
1302+
1303+ test ( "flow_reset_session clears persisted session state and docs" , async ( ) => {
1304+ const worktree = makeTempDir ( ) ;
1305+ const tools = createTools ( { } ) as any ;
1306+ await saveSession ( worktree , createSession ( "Build a workflow plugin" ) ) ;
1307+
1308+ const response = await tools . flow_reset_session . execute ( { } , { worktree } ) ;
1309+ const parsed = JSON . parse ( response ) ;
1310+
1311+ expect ( parsed . status ) . toBe ( "ok" ) ;
1312+ expect ( parsed . nextCommand ) . toBe ( "/flow-plan <goal>" ) ;
1313+ expect ( await loadSession ( worktree ) ) . toBeNull ( ) ;
1314+ await expect ( readFile ( getIndexDocPath ( worktree ) , "utf8" ) ) . rejects . toThrow ( ) ;
1315+ } ) ;
1316+
1317+ test ( "tools return machine-readable missing-session responses for plan, review, and reset operations" , async ( ) => {
1318+ const worktree = makeTempDir ( ) ;
1319+ const tools = createTools ( { } ) as any ;
1320+ const cases = [
1321+ [ "flow_plan_apply" , { plan : samplePlan ( ) } , "missing_session" , "/flow-plan <goal>" ] ,
1322+ [ "flow_plan_approve" , { } , "missing_session" , undefined ] ,
1323+ [ "flow_plan_select_features" , { featureIds : [ "setup-runtime" ] } , "missing_session" , undefined ] ,
1324+ [ "flow_review_record_feature" , { scope : "feature" , featureId : "setup-runtime" , status : "approved" , summary : "Looks good." } , "missing_session" , undefined ] ,
1325+ [ "flow_review_record_final" , { scope : "final" , status : "approved" , summary : "Looks good." } , "missing_session" , undefined ] ,
1326+ [ "flow_reset_feature" , { featureId : "setup-runtime" } , "missing_session" , undefined ] ,
1327+ ] as const ;
1328+
1329+ for ( const [ toolName , args , expectedStatus , expectedNextCommand ] of cases ) {
1330+ const response = await tools [ toolName ] . execute ( args , { worktree } ) ;
1331+ const parsed = JSON . parse ( response ) ;
1332+
1333+ expect ( parsed . status ) . toBe ( expectedStatus ) ;
1334+ expect ( parsed . summary ) . toContain ( "No active Flow" ) ;
1335+ if ( expectedNextCommand ) {
1336+ expect ( parsed . nextCommand ) . toBe ( expectedNextCommand ) ;
1337+ }
1338+ }
1339+ } ) ;
1340+
12031341 test ( "tool rejects flow_run_start for completed sessions" , async ( ) => {
12041342 const worktree = makeTempDir ( ) ;
12051343 const tools = createTools ( { } ) as any ;
0 commit comments