(spec: E2ECase)
| 137 | } |
| 138 | |
| 139 | async function runCase(spec: E2ECase): Promise<CaseResult> { |
| 140 | const errors: string[] = []; |
| 141 | const samples: CaseResult["samples"] = []; |
| 142 | const t0 = Date.now(); |
| 143 | |
| 144 | const sampleIntervalMs = spec.sampleIntervalMs ?? 1000; |
| 145 | const maxWaitMs = spec.maxWaitMs ?? 30_000; |
| 146 | |
| 147 | // Drain stale updates so we don't accidentally match a previous run's reply. |
| 148 | const drainFence = await drainUpdates(); |
| 149 | |
| 150 | if (AUTOMATED) { |
| 151 | // Automated mode: sender bot sends the prompt. |
| 152 | await sendMessageAsSenderBot(TEST_CHAT_ID, spec.prompt).catch((e: Error) => |
| 153 | errors.push(`send failed: ${e.message}`), |
| 154 | ); |
| 155 | } else { |
| 156 | // Manual-trigger mode: give the operator 15 s to send the prompt manually. |
| 157 | // This wait is BEFORE we start polling — the bot won't have replied yet. |
| 158 | await waitForOperator(spec.prompt, 15_000); |
| 159 | } |
| 160 | |
| 161 | const onSample = (s: { elapsedMs: number; text: string | undefined }) => { |
| 162 | const text = s.text ?? ""; |
| 163 | const balanced = isBalanced(text); |
| 164 | samples.push({ |
| 165 | elapsedMs: s.elapsedMs, |
| 166 | balanced, |
| 167 | len: text.length, |
| 168 | preview: text.slice(0, 100), |
| 169 | ...(text.length > 0 && !balanced ? { full: text } : {}), |
| 170 | }); |
| 171 | }; |
| 172 | |
| 173 | const result = await watchForReply({ |
| 174 | chatId: TEST_CHAT_ID, |
| 175 | sinceUpdateId: drainFence, |
| 176 | intervalMs: sampleIntervalMs, |
| 177 | timeoutMs: maxWaitMs, |
| 178 | onSample, |
| 179 | }); |
| 180 | |
| 181 | // Capture the highest update_id consumed so the follow-up baseline is |
| 182 | // correct. getUpdates is destructive (advancing the offset confirms/deletes |
| 183 | // prior updates server-side), so we must NOT reuse drainFence here. |
| 184 | const firstReplyFence = result.reachedUpdateId; |
| 185 | |
| 186 | const finalText = result.finalText; |
| 187 | const exp = spec.expectations ?? {}; |
| 188 | runExpectations(exp, finalText, errors); |
| 189 | |
| 190 | const unbalancedSamples = samples.filter( |
| 191 | (s) => s.len > 0 && !s.balanced, |
| 192 | ).length; |
| 193 | if (exp.balancedBrackets && unbalancedSamples > 0) { |
| 194 | errors.push(`${unbalancedSamples} mid-stream samples were not balanced`); |
| 195 | } |
| 196 |
no test coverage detected