@@ -110,6 +110,139 @@ describe("lobster plugin tool", () => {
110110 expect ( approval . resumeToken ) . toBe ( "resume-token-1" ) ;
111111 } ) ;
112112
113+ it ( "keeps ordinary run on the embedded runner when flow defaults are injected" , async ( ) => {
114+ const runner = {
115+ run : vi . fn ( ) . mockResolvedValue ( {
116+ ok : true ,
117+ status : "needs_approval" ,
118+ output : [ ] ,
119+ requiresApproval : {
120+ type : "approval_request" ,
121+ prompt : "Continue?" ,
122+ items : [ ] ,
123+ resumeToken : "resume-token-1" ,
124+ } ,
125+ } ) ,
126+ } ;
127+ const taskFlow = createFakeTaskFlow ( ) ;
128+
129+ const tool = createLobsterTool ( fakeApi ( ) , { runner, taskFlow } ) ;
130+ const res = await tool . execute ( "call-default-flow-run" , {
131+ action : "run" ,
132+ pipeline : "noop" ,
133+ flowStateJson : "{}" ,
134+ flowExpectedRevision : 0 ,
135+ } ) ;
136+
137+ expect ( taskFlow . createManaged ) . not . toHaveBeenCalled ( ) ;
138+ expect ( runner . run ) . toHaveBeenCalledWith ( {
139+ action : "run" ,
140+ pipeline : "noop" ,
141+ cwd : process . cwd ( ) ,
142+ timeoutMs : 20_000 ,
143+ maxStdoutBytes : 512_000 ,
144+ } ) ;
145+ const details = requireRecord ( res . details , "ordinary run with flow defaults details" ) ;
146+ expect ( details . status ) . toBe ( "needs_approval" ) ;
147+ } ) ;
148+
149+ it . each ( [ { flowId : "flow-1" } , { flowExpectedRevision : 1 } ] ) (
150+ "rejects resume-only fields on run before the ordinary fallback" ,
151+ async ( resumeFields ) => {
152+ const runner = { run : vi . fn ( ) } ;
153+ const tool = createLobsterTool ( fakeApi ( ) , {
154+ runner,
155+ taskFlow : createFakeTaskFlow ( ) ,
156+ } ) ;
157+
158+ await expect (
159+ tool . execute ( "call-run-with-resume-fields" , {
160+ action : "run" ,
161+ pipeline : "noop" ,
162+ flowStateJson : "{}" ,
163+ flowExpectedRevision : 0 ,
164+ ...resumeFields ,
165+ } ) ,
166+ ) . rejects . toThrow ( / r u n a c t i o n d o e s n o t a c c e p t f l o w I d o r f l o w E x p e c t e d R e v i s i o n / ) ;
167+ expect ( runner . run ) . not . toHaveBeenCalled ( ) ;
168+ } ,
169+ ) ;
170+
171+ it ( "keeps ordinary resume on the embedded runner when flow defaults are injected" , async ( ) => {
172+ const runner = {
173+ run : vi . fn ( ) . mockResolvedValue ( {
174+ ok : true ,
175+ status : "ok" ,
176+ output : [ { approved : true } ] ,
177+ requiresApproval : null ,
178+ } ) ,
179+ } ;
180+ const taskFlow = createFakeTaskFlow ( ) ;
181+
182+ const tool = createLobsterTool ( fakeApi ( ) , { runner, taskFlow } ) ;
183+ const res = await tool . execute ( "call-default-flow-resume" , {
184+ action : "resume" ,
185+ token : "resume-token-1" ,
186+ approve : true ,
187+ flowStateJson : "{}" ,
188+ flowExpectedRevision : 0 ,
189+ } ) ;
190+
191+ expect ( taskFlow . resume ) . not . toHaveBeenCalled ( ) ;
192+ expect ( runner . run ) . toHaveBeenCalledWith ( {
193+ action : "resume" ,
194+ token : "resume-token-1" ,
195+ approve : true ,
196+ cwd : process . cwd ( ) ,
197+ timeoutMs : 20_000 ,
198+ maxStdoutBytes : 512_000 ,
199+ } ) ;
200+ const details = requireRecord ( res . details , "ordinary resume with flow defaults details" ) ;
201+ expect ( details . ok ) . toBe ( true ) ;
202+ expect ( details . status ) . toBe ( "ok" ) ;
203+ } ) ;
204+
205+ it . each ( [
206+ { flowControllerId : "tests/lobster" } ,
207+ { flowGoal : "Run Lobster workflow" } ,
208+ { flowStateJson : '{"lane":"email"}' } ,
209+ ] ) ( "rejects run-only fields on resume before the ordinary fallback" , async ( runFields ) => {
210+ const runner = { run : vi . fn ( ) } ;
211+ const tool = createLobsterTool ( fakeApi ( ) , {
212+ runner,
213+ taskFlow : createFakeTaskFlow ( ) ,
214+ } ) ;
215+
216+ await expect (
217+ tool . execute ( "call-resume-with-run-fields" , {
218+ action : "resume" ,
219+ token : "resume-token-1" ,
220+ approve : true ,
221+ flowExpectedRevision : 0 ,
222+ ...runFields ,
223+ } ) ,
224+ ) . rejects . toThrow ( / r e s u m e a c t i o n d o e s n o t a c c e p t f l o w C o n t r o l l e r I d , f l o w G o a l , o r f l o w S t a t e J s o n / ) ;
225+ expect ( runner . run ) . not . toHaveBeenCalled ( ) ;
226+ } ) ;
227+
228+ it ( "rejects resume with a non-default flow revision but no flowId" , async ( ) => {
229+ const runner = { run : vi . fn ( ) } ;
230+ const tool = createLobsterTool ( fakeApi ( ) , {
231+ runner,
232+ taskFlow : createFakeTaskFlow ( ) ,
233+ } ) ;
234+
235+ await expect (
236+ tool . execute ( "call-revision-without-flow-id" , {
237+ action : "resume" ,
238+ token : "resume-token-1" ,
239+ approve : true ,
240+ flowExpectedRevision : 1 ,
241+ } ) ,
242+ ) . rejects . toThrow ( / f l o w I d r e q u i r e d w h e n u s i n g m a n a g e d T a s k F l o w r e s u m e m o d e / ) ;
243+ expect ( runner . run ) . not . toHaveBeenCalled ( ) ;
244+ } ) ;
245+
113246 it ( "normalizes numeric string run limits before invoking the runner" , async ( ) => {
114247 const runner = {
115248 run : vi . fn ( ) . mockResolvedValue ( {
@@ -203,6 +336,7 @@ describe("lobster plugin tool", () => {
203336 flowControllerId : "tests/lobster" ,
204337 flowGoal : "Run Lobster workflow" ,
205338 flowStateJson : '{"lane":"email"}' ,
339+ flowExpectedRevision : 0 ,
206340 flowCurrentStep : "run_lobster" ,
207341 flowWaitingStep : "await_review" ,
208342 } ) ;
@@ -234,6 +368,41 @@ describe("lobster plugin tool", () => {
234368 expect ( mutation . applied ) . toBe ( true ) ;
235369 } ) ;
236370
371+ it ( "preserves explicit empty flow state in managed TaskFlow run mode" , async ( ) => {
372+ const runner = {
373+ run : vi . fn ( ) . mockResolvedValue ( {
374+ ok : true ,
375+ status : "ok" ,
376+ output : [ ] ,
377+ requiresApproval : null ,
378+ } ) ,
379+ } ;
380+ const taskFlow = createFakeTaskFlow ( ) ;
381+
382+ const tool = createLobsterTool ( fakeApi ( ) , { runner, taskFlow } ) ;
383+ await tool . execute ( "call-managed-run-empty-state" , {
384+ action : "run" ,
385+ pipeline : "noop" ,
386+ flowControllerId : "tests/lobster" ,
387+ flowGoal : "Run Lobster workflow" ,
388+ flowStateJson : "{}" ,
389+ } ) ;
390+
391+ expect ( taskFlow . createManaged ) . toHaveBeenCalledWith ( {
392+ controllerId : "tests/lobster" ,
393+ goal : "Run Lobster workflow" ,
394+ currentStep : "run_lobster" ,
395+ stateJson : { } ,
396+ } ) ;
397+ expect ( runner . run ) . toHaveBeenCalledWith ( {
398+ action : "run" ,
399+ pipeline : "noop" ,
400+ cwd : process . cwd ( ) ,
401+ timeoutMs : 20_000 ,
402+ maxStdoutBytes : 512_000 ,
403+ } ) ;
404+ } ) ;
405+
237406 it ( "rejects managed TaskFlow params when no bound taskFlow runtime is available" , async ( ) => {
238407 const tool = createLobsterTool ( fakeApi ( ) , {
239408 runner : { run : vi . fn ( ) } ,
@@ -284,6 +453,7 @@ describe("lobster plugin tool", () => {
284453 approve : true ,
285454 flowId : "flow-1" ,
286455 flowExpectedRevision : 1 ,
456+ flowStateJson : "{}" ,
287457 flowCurrentStep : "resume_lobster" ,
288458 } ) ;
289459
0 commit comments