@@ -2,10 +2,11 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
22import type { Adapters } from "./adapters.js" ;
33import type { TicketContent } from "../adapters/issue-tracker/types.js" ;
44
5-
65const mockStart = vi . fn ( ) ;
6+ const mockGetRun = vi . fn ( ) ;
77vi . mock ( "workflow/api" , ( ) => ( {
88 start : ( ...args : any [ ] ) => mockStart ( ...args ) ,
9+ getRun : ( ...args : any [ ] ) => mockGetRun ( ...args ) ,
910} ) ) ;
1011
1112vi . mock ( "../workflows/implementation.js" , ( ) => ( {
@@ -23,7 +24,6 @@ vi.mock("@vercel/sandbox", () => ({
2324 } ,
2425} ) ) ;
2526
26-
2727function makeTicket ( overrides : Partial < TicketContent > = { } ) : TicketContent {
2828 return {
2929 id : "ticket-001" ,
@@ -38,16 +38,22 @@ function makeTicket(overrides: Partial<TicketContent> = {}): TicketContent {
3838 } ;
3939}
4040
41- function makeAdapters ( overrides : Partial < {
42- claim : ReturnType < typeof vi . fn > ;
43- register : ReturnType < typeof vi . fn > ;
44- unregister : ReturnType < typeof vi . fn > ;
45- fetchTicket : ReturnType < typeof vi . fn > ;
46- findPR : ReturnType < typeof vi . fn > ;
47- } > = { } ) : Adapters {
41+ function makeAdapters (
42+ overrides : Partial < {
43+ claim : ReturnType < typeof vi . fn > ;
44+ register : ReturnType < typeof vi . fn > ;
45+ unregister : ReturnType < typeof vi . fn > ;
46+ getRunId : ReturnType < typeof vi . fn > ;
47+ fetchTicket : ReturnType < typeof vi . fn > ;
48+ findPR : ReturnType < typeof vi . fn > ;
49+ } > = { } ,
50+ ) : Adapters {
51+ let claimedValue : string | undefined ;
52+
4853 return {
4954 issueTracker : {
50- fetchTicket : overrides . fetchTicket ?? vi . fn ( ) . mockResolvedValue ( makeTicket ( ) ) ,
55+ fetchTicket :
56+ overrides . fetchTicket ?? vi . fn ( ) . mockResolvedValue ( makeTicket ( ) ) ,
5157 moveTicket : vi . fn ( ) ,
5258 postComment : vi . fn ( ) ,
5359 searchTickets : vi . fn ( ) ,
@@ -64,16 +70,22 @@ function makeAdapters(overrides: Partial<{
6470 notify : vi . fn ( ) ,
6571 } ,
6672 runRegistry : {
67- claim : overrides . claim ?? vi . fn ( ) . mockResolvedValue ( true ) ,
73+ claim :
74+ overrides . claim ??
75+ vi . fn ( ) . mockImplementation ( async ( _key : string , value : string ) => {
76+ claimedValue = value ;
77+ return true ;
78+ } ) ,
6879 register : overrides . register ?? vi . fn ( ) . mockResolvedValue ( undefined ) ,
6980 unregister : overrides . unregister ?? vi . fn ( ) . mockResolvedValue ( undefined ) ,
70- getRunId : vi . fn ( ) ,
81+ getRunId :
82+ overrides . getRunId ??
83+ vi . fn ( ) . mockImplementation ( async ( ) => claimedValue ) ,
7184 listAll : vi . fn ( ) ,
7285 } ,
7386 } ;
7487}
7588
76-
7789describe ( "dispatchTicket" , ( ) => {
7890 beforeEach ( ( ) => {
7991 vi . clearAllMocks ( ) ;
@@ -90,24 +102,44 @@ describe("dispatchTicket", () => {
90102 const result = await dispatchTicket ( "PROJ-42" , adapters , 5 ) ;
91103
92104 expect ( result ) . toEqual ( { started : true , runId : "run_123" } ) ;
93- expect ( adapters . runRegistry . claim ) . toHaveBeenCalledWith ( "PROJ-42" , "claiming" ) ;
105+ expect ( adapters . runRegistry . claim ) . toHaveBeenCalledWith (
106+ "PROJ-42" ,
107+ expect . stringMatching ( / ^ c l a i m i n g : \d + $ / ) ,
108+ ) ;
94109 expect ( adapters . issueTracker . fetchTicket ) . toHaveBeenCalledWith ( "PROJ-42" ) ;
95110 expect ( adapters . vcs . findPR ) . toHaveBeenCalledWith ( "blazebot/proj-42" ) ;
96- expect ( mockStart ) . toHaveBeenCalledWith ( "implementationWorkflow_sentinel" , [ "ticket-001" ] ) ;
97- expect ( adapters . runRegistry . register ) . toHaveBeenCalledWith ( "PROJ-42" , "run_123" ) ;
111+ expect ( mockStart ) . toHaveBeenCalledWith ( "implementationWorkflow_sentinel" , [
112+ "ticket-001" ,
113+ ] ) ;
114+ expect ( adapters . runRegistry . register ) . toHaveBeenCalledWith (
115+ "PROJ-42" ,
116+ "run_123" ,
117+ ) ;
98118 } ) ;
99119
100120 it ( "dispatches review-fix workflow when PR exists" , async ( ) => {
101121 const adapters = makeAdapters ( {
102- findPR : vi . fn ( ) . mockResolvedValue ( { id : 7 , url : "https://github.com/pr/7" , branch : "blazebot/proj-42" } ) ,
122+ findPR : vi
123+ . fn ( )
124+ . mockResolvedValue ( {
125+ id : 7 ,
126+ url : "https://github.com/pr/7" ,
127+ branch : "blazebot/proj-42" ,
128+ } ) ,
103129 } ) ;
104130 const { dispatchTicket } = await import ( "./dispatch.js" ) ;
105131
106132 const result = await dispatchTicket ( "PROJ-42" , adapters , 5 ) ;
107133
108134 expect ( result ) . toEqual ( { started : true , runId : "run_123" } ) ;
109- expect ( mockStart ) . toHaveBeenCalledWith ( "reviewFixWorkflow_sentinel" , [ "ticket-001" , "blazebot/proj-42" ] ) ;
110- expect ( adapters . runRegistry . register ) . toHaveBeenCalledWith ( "PROJ-42" , "run_123" ) ;
135+ expect ( mockStart ) . toHaveBeenCalledWith ( "reviewFixWorkflow_sentinel" , [
136+ "ticket-001" ,
137+ "blazebot/proj-42" ,
138+ ] ) ;
139+ expect ( adapters . runRegistry . register ) . toHaveBeenCalledWith (
140+ "PROJ-42" ,
141+ "run_123" ,
142+ ) ;
111143 } ) ;
112144
113145 it ( "returns already_claimed when claim fails" , async ( ) => {
@@ -143,6 +175,25 @@ describe("dispatchTicket", () => {
143175 expect ( mockStart ) . not . toHaveBeenCalled ( ) ;
144176 } ) ;
145177
178+ it ( "aborts workflow if claim was removed during dispatch" , async ( ) => {
179+ const mockCancel = vi . fn ( ) . mockResolvedValue ( undefined ) ;
180+ mockGetRun . mockReturnValue ( { cancel : mockCancel } ) ;
181+
182+ // getRunId returns null — claim was removed by a cancel while workflow was starting
183+ const adapters = makeAdapters ( {
184+ getRunId : vi . fn ( ) . mockResolvedValue ( null ) ,
185+ } ) ;
186+ const { dispatchTicket } = await import ( "./dispatch.js" ) ;
187+
188+ const result = await dispatchTicket ( "PROJ-42" , adapters , 5 ) ;
189+
190+ expect ( result ) . toEqual ( { started : false , reason : "already_claimed" } ) ;
191+ expect ( mockStart ) . toHaveBeenCalled ( ) ;
192+ expect ( mockGetRun ) . toHaveBeenCalledWith ( "run_123" ) ;
193+ expect ( mockCancel ) . toHaveBeenCalled ( ) ;
194+ expect ( adapters . runRegistry . register ) . not . toHaveBeenCalled ( ) ;
195+ } ) ;
196+
146197 it ( "unregisters claim and returns error on dispatch failure" , async ( ) => {
147198 const unregister = vi . fn ( ) . mockResolvedValue ( undefined ) ;
148199 const adapters = makeAdapters ( {
0 commit comments