@@ -1175,6 +1175,79 @@ describe("dispatchCronDelivery — double-announce guard", () => {
|
1175 | 1175 | expect(retireSessionMcpRuntime).not.toHaveBeenCalled(); |
1176 | 1176 | }); |
1177 | 1177 | |
| 1178 | +it("cleans up the direct cron session when delivery target resolution is refused (deleteAfterRun)", async () => { |
| 1179 | +// A keyless implicit cron whose inherited shared-bucket target is refused |
| 1180 | +// (resolvedDelivery.ok=false, issue #91613 fail-closed path) must still |
| 1181 | +// retire its session/transcript when deleteAfterRun is enabled — otherwise |
| 1182 | +// the one-shot session leaks. |
| 1183 | +const params = makeBaseParams({ synthesizedText: "refused report" }); |
| 1184 | +params.resolvedDelivery = { |
| 1185 | +ok: false, |
| 1186 | +channel: "telegram", |
| 1187 | +to: undefined, |
| 1188 | +accountId: undefined, |
| 1189 | +threadId: undefined, |
| 1190 | +mode: "implicit", |
| 1191 | +error: new Error("refusing inherited shared-bucket delivery target"), |
| 1192 | +}; |
| 1193 | +params.agentSessionKey = "agent:main:cron:test-job"; |
| 1194 | +(params.job as { deleteAfterRun?: boolean }).deleteAfterRun = true; |
| 1195 | + |
| 1196 | +const state = await dispatchCronDelivery(params); |
| 1197 | + |
| 1198 | +expect(deliverOutboundPayloads).not.toHaveBeenCalled(); |
| 1199 | +expectResultFields(state.result, { |
| 1200 | +status: "error", |
| 1201 | +errorKind: "delivery-target", |
| 1202 | +}); |
| 1203 | +expect(callGateway).toHaveBeenCalledWith({ |
| 1204 | +method: "sessions.delete", |
| 1205 | +params: { |
| 1206 | +key: "agent:main:cron:test-job", |
| 1207 | +deleteTranscript: true, |
| 1208 | +emitLifecycleHooks: false, |
| 1209 | +}, |
| 1210 | +timeoutMs: 10_000, |
| 1211 | +}); |
| 1212 | +}); |
| 1213 | + |
| 1214 | +it("cleans up the direct cron session when refused delivery is best-effort (deleteAfterRun)", async () => { |
| 1215 | +// Same fail-closed refusal, best-effort variant: dispatch returns status:ok |
| 1216 | +// (warn-logs instead of failing the run) but the deleteAfterRun session must |
| 1217 | +// still be retired. |
| 1218 | +const params = makeBaseParams({ |
| 1219 | +synthesizedText: "refused report", |
| 1220 | +deliveryBestEffort: true, |
| 1221 | +}); |
| 1222 | +params.resolvedDelivery = { |
| 1223 | +ok: false, |
| 1224 | +channel: "telegram", |
| 1225 | +to: undefined, |
| 1226 | +accountId: undefined, |
| 1227 | +threadId: undefined, |
| 1228 | +mode: "implicit", |
| 1229 | +error: new Error("refusing inherited shared-bucket delivery target"), |
| 1230 | +}; |
| 1231 | +params.agentSessionKey = "agent:main:cron:test-job"; |
| 1232 | +(params.job as { deleteAfterRun?: boolean }).deleteAfterRun = true; |
| 1233 | + |
| 1234 | +const state = await dispatchCronDelivery(params); |
| 1235 | + |
| 1236 | +expect(deliverOutboundPayloads).not.toHaveBeenCalled(); |
| 1237 | +expectResultFields(state.result, { |
| 1238 | +status: "ok", |
| 1239 | +}); |
| 1240 | +expect(callGateway).toHaveBeenCalledWith({ |
| 1241 | +method: "sessions.delete", |
| 1242 | +params: { |
| 1243 | +key: "agent:main:cron:test-job", |
| 1244 | +deleteTranscript: true, |
| 1245 | +emitLifecycleHooks: false, |
| 1246 | +}, |
| 1247 | +timeoutMs: 10_000, |
| 1248 | +}); |
| 1249 | +}); |
| 1250 | + |
1178 | 1251 | it("text delivery fires exactly once (no double-deliver)", async () => { |
1179 | 1252 | vi.mocked(deliverOutboundPayloads).mockResolvedValue([{ ok: true } as never]); |
1180 | 1253 | |
|