@@ -137,13 +137,14 @@ type sentMessage struct {
137137}
138138
139139type fakeMessenger struct {
140- mu sync.Mutex
141- messages []sentMessage
142- nextID int
143- sendTextErr error
144- sendCardErr error
145- updateCardErr error
146- deleteCardErr error
140+ mu sync.Mutex
141+ messages []sentMessage
142+ nextID int
143+ sendTextErr error
144+ sendCardErr error
145+ updateCardErr error
146+ deleteCardErr error
147+ sendPermissionCardHook func (cardID string , payload PermissionCardPayload )
147148}
148149
149150func (m * fakeMessenger ) SendText (_ context.Context , chatID string , text string ) error {
@@ -155,10 +156,14 @@ func (m *fakeMessenger) SendText(_ context.Context, chatID string, text string)
155156
156157func (m * fakeMessenger ) SendPermissionCard (_ context.Context , chatID string , payload PermissionCardPayload ) (string , error ) {
157158 m .mu .Lock ()
158- defer m .mu .Unlock ()
159159 m .nextID ++
160160 cardID := fmt .Sprintf ("perm-card-%d" , m .nextID )
161161 m .messages = append (m .messages , sentMessage {chatID : chatID , kind : "card" , card : payload , cardID : cardID })
162+ hook := m .sendPermissionCardHook
163+ m .mu .Unlock ()
164+ if hook != nil {
165+ hook (cardID , payload )
166+ }
162167 return cardID , nil
163168}
164169
@@ -2311,6 +2316,72 @@ func TestApprovalOutboxPreflightDropsStaleOperation(t *testing.T) {
23112316 }
23122317}
23132318
2319+ func TestApprovalOutboxSendCardCleanupWhenVersionAdvancedDuringSend (t * testing.T ) {
2320+ adapter := newTestAdapter (t )
2321+ sessionID := "session-outbox-race-send"
2322+ runID := "run-outbox-race-send"
2323+ runKey := runBindingKey (sessionID , runID )
2324+ adapter .trackSession (sessionID , runID , "chat-outbox-race-send" , "outbox race send task" )
2325+
2326+ var createdCardID string
2327+ var hookOnce sync.Once
2328+ messenger := adapterTestMessenger (adapter )
2329+ messenger .mu .Lock ()
2330+ messenger .sendPermissionCardHook = func (cardID string , _ PermissionCardPayload ) {
2331+ createdCardID = strings .TrimSpace (cardID )
2332+ hookOnce .Do (func () {
2333+ adapter .mu .Lock ()
2334+ if fsm := adapter .approvalFSMByRun [runKey ]; fsm != nil {
2335+ fsm .Version ++
2336+ }
2337+ adapter .mu .Unlock ()
2338+ })
2339+ }
2340+ messenger .mu .Unlock ()
2341+
2342+ adapter .processPermissionRequested (
2343+ context .Background (),
2344+ sessionID ,
2345+ runID ,
2346+ "chat-outbox-race-send" ,
2347+ "perm-outbox-race-send" ,
2348+ "filesystem_write_file" ,
2349+ "write_file" ,
2350+ "outbox-race-send.txt" ,
2351+ "需要审批" ,
2352+ )
2353+ if strings .TrimSpace (createdCardID ) == "" {
2354+ t .Fatal ("expected permission card to be sent" )
2355+ }
2356+
2357+ msgs := adapterTestMessenger (adapter ).snapshot ()
2358+ foundDelete := false
2359+ for _ , message := range msgs {
2360+ if message .kind == "delete_card" && message .chatID == createdCardID {
2361+ foundDelete = true
2362+ break
2363+ }
2364+ }
2365+ if ! foundDelete {
2366+ t .Fatalf ("expected stale sent permission card to be deleted, msgs=%#v" , msgs )
2367+ }
2368+
2369+ adapter .mu .RLock ()
2370+ fsm := adapter .approvalFSMByRun [runKey ]
2371+ storedCardID := ""
2372+ if fsm != nil {
2373+ storedCardID = strings .TrimSpace (fsm .CardID )
2374+ }
2375+ _ , indexed := adapter .approvalCardRunIndex [createdCardID ]
2376+ adapter .mu .RUnlock ()
2377+ if storedCardID != "" {
2378+ t .Fatalf ("stale card should not be attached to fsm, got %q" , storedCardID )
2379+ }
2380+ if indexed {
2381+ t .Fatalf ("stale card should not remain in approvalCardRunIndex: %s" , createdCardID )
2382+ }
2383+ }
2384+
23142385func TestTryHandleTextPermissionHandlesAskUserAnswerAndSkip (t * testing.T ) {
23152386 adapter := newTestAdapter (t )
23162387 sessionID := BuildSessionID ("chat-ask-text-cmd" )
0 commit comments