




















@@ -0,0 +1,158 @@
1+# Cron natural fire no duplicate
2+3+```yaml qa-scenario
4+id: cron-natural-fire-no-duplicate
5+title: Cron natural fire no duplicate
6+surface: cron
7+coverage:
8+primary:
9+ - scheduling.cron
10+secondary:
11+ - channels.qa-channel
12+ - scheduling.dedup
13+objective: Verify one naturally fired cron run in a single gateway uptime produces exactly one qa-channel delivery for its marker.
14+successCriteria:
15+ - A one-shot cron job fires from the scheduler timer without cron.run force mode.
16+ - The qa-channel receives exactly one outbound reply containing the run marker.
17+ - No second outbound reply with the same marker appears during the duplicate window.
18+docsRefs:
19+ - docs/help/testing.md
20+ - docs/channels/qa-channel.md
21+codeRefs:
22+ - src/cron/service.ts
23+ - src/cron/service/timer.ts
24+ - src/cron/run-log.ts
25+ - extensions/qa-lab/src/cron-run-wait.ts
26+ - extensions/qa-lab/src/suite-runtime-transport.ts
27+execution:
28+kind: flow
29+summary: Let one cron job fire from the natural scheduler timer and assert qa-channel does not receive a duplicate delivery for the same marker.
30+config:
31+channelId: qa-room
32+channelTitle: QA Room
33+fireDelayMs: 12000
34+duplicateWindowMs: 8000
35+reminderPromptTemplate: "A natural QA cron dedupe check fired. Send a one-line ping back to the room containing this exact marker: {{marker}}"
36+```
37+38+```yaml qa-flow
39+steps:
40+ - name: creates a near-future cron job and waits for the scheduler timer
41+actions:
42+ - call: reset
43+ - set: runStartedAt
44+value:
45+expr: "Date.now()"
46+ - set: scheduledFor
47+value:
48+expr: "new Date(runStartedAt + config.fireDelayMs).toISOString()"
49+ - set: cronMarker
50+value:
51+expr: "`QA-CRON-NATURAL-DEDUPE-${randomUUID().slice(0, 8)}`"
52+ - call: env.gateway.call
53+saveAs: response
54+args:
55+ - cron.add
56+ - name:
57+expr: "`qa-natural-dedupe-${randomUUID()}`"
58+enabled: true
59+schedule:
60+kind: at
61+at:
62+ref: scheduledFor
63+sessionTarget: isolated
64+wakeMode: now
65+payload:
66+kind: agentTurn
67+message:
68+expr: "config.reminderPromptTemplate.replace('{{marker}}', cronMarker)"
69+delivery:
70+mode: announce
71+channel: qa-channel
72+to:
73+expr: "`channel:${config.channelId}`"
74+ - timeoutMs: 30000
75+ - set: jobId
76+value:
77+expr: response.id
78+ - assert:
79+expr: "Boolean(jobId)"
80+message: missing cron job id
81+ - set: scheduledAtMs
82+value:
83+expr: "new Date(response.schedule?.at ?? scheduledFor).getTime()"
84+ - set: scheduleDeltaMs
85+value:
86+expr: "scheduledAtMs - runStartedAt"
87+ - assert:
88+expr: "scheduleDeltaMs >= config.fireDelayMs - 2000 && scheduleDeltaMs <= config.fireDelayMs + 5000"
89+message:
90+expr: "`expected near-future natural fire, got ${scheduleDeltaMs}ms`"
91+ - call: waitForCronRunCompletion
92+saveAs: completedRun
93+args:
94+ - callGateway:
95+expr: "env.gateway.call.bind(env.gateway)"
96+jobId:
97+ref: jobId
98+afterTs:
99+ref: runStartedAt
100+timeoutMs:
101+expr: "liveTurnTimeoutMs(env, Math.max(60000, config.fireDelayMs + 45000))"
102+ - assert:
103+expr: "Date.now() >= scheduledAtMs"
104+message:
105+expr: "`cron completed before scheduled time ${scheduledFor}`"
106+ - assert:
107+expr: "completedRun?.status === 'ok'"
108+message:
109+expr: "`expected natural cron run ok, got ${JSON.stringify(completedRun)}`"
110+detailsExpr: "`job=${jobId} marker=${cronMarker} scheduled=${scheduledFor}`"
111+112+ - name: observes exactly one qa-channel delivery for the natural run
113+actions:
114+ - call: waitForOutboundMessage
115+saveAs: firstOutbound
116+args:
117+ - ref: state
118+ - lambda:
119+params: [candidate]
120+expr: "candidate.conversation.id === config.channelId && candidate.text.includes(cronMarker)"
121+ - expr: liveTurnTimeoutMs(env, 45000)
122+ - set: firstOutboundId
123+value:
124+expr: firstOutbound.id
125+ - set: firstOutboundIndex
126+value:
127+expr: "getTransportSnapshot().messages.findIndex((message) => message.id === firstOutboundId)"
128+ - assert:
129+expr: "firstOutboundIndex >= 0"
130+message: first outbound message missing from qa-channel snapshot
131+ - call: sleep
132+args:
133+ - expr: config.duplicateWindowMs
134+ - set: duplicateMatches
135+value:
136+expr: "getTransportSnapshot().messages.filter((message) => message.direction === 'outbound' && message.conversation.id === config.channelId && message.text.includes(cronMarker))"
137+ - assert:
138+expr: "duplicateMatches.length === 1"
139+message:
140+expr: "`expected one natural outbound delivery for ${cronMarker}, saw ${duplicateMatches.length}: ${duplicateMatches.map((message) => message.text).join(' | ')}`"
141+ - call: env.gateway.call
142+saveAs: runsPage
143+args:
144+ - cron.runs
145+ - id:
146+ref: jobId
147+limit: 10
148+sortDir: desc
149+ - timeoutMs: 30000
150+ - set: completedRuns
151+value:
152+expr: "runsPage.entries.filter((entry) => entry.ts >= runStartedAt && ['ok', 'error', 'skipped'].includes(entry.status))"
153+ - assert:
154+expr: "completedRuns.length === 1"
155+message:
156+expr: "`expected one completed natural cron run for ${jobId}, saw ${completedRuns.length}: ${JSON.stringify(completedRuns)}`"
157+detailsExpr: "`first outbound=${firstOutboundId}; duplicate window=${config.duplicateWindowMs}ms`"
158+```
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。