(sessionId: string, prompt: string, text: string, usage: MockUsage = LOW_USAGE)
| 249 | |
| 250 | let turnCounter = 0; |
| 251 | async function send(sessionId: string, prompt: string, text: string, usage: MockUsage = LOW_USAGE): Promise<void> { |
| 252 | h.mock.setDefault({ text, usage }); |
| 253 | const turn = ++turnCounter; |
| 254 | const reqsBefore = h.mock.requests().length; |
| 255 | const startedAt = Date.now(); |
| 256 | // [LR-DIAG] console.error flushes immediately on CI even when console.log is buffered |
| 257 | console.error(`[LR-DIAG] turn ${turn} START prompt="${prompt.slice(0, 60)}" mockReqs=${reqsBefore}`); |
| 258 | |
| 259 | // OC-DIAG: every 50 mock requests, query opencode's session DB to see |
| 260 | // what its filterCompacted/latest() actually returns. This tells us why |
| 261 | // OpenCode's loop break condition isn't firing. |
| 262 | let lastDumpAt = 0; |
| 263 | // Runaway detector: if a single turn generates >100 mock requests, it's |
| 264 | // pathological. Normal turns generate 1-3 requests. Fire 1 diagnostic |
| 265 | // then fail the test fast — much better than waiting for 600s timeout. |
| 266 | let runawayAborted = false; |
| 267 | const dumpTimer = setInterval(() => { |
| 268 | const now = Date.now(); |
| 269 | const reqs = h.mock.requests().length; |
| 270 | // Fire diagnostic as soon as we see >=25 unexpected requests, then |
| 271 | // every 5s while the hang persists. The earlier we capture data the |
| 272 | // less likely it is to be lost to a job-level CI timeout. |
| 273 | if (reqs > reqsBefore + 25 && now - lastDumpAt > 5000) { |
| 274 | lastDumpAt = now; |
| 275 | // Open opencode's session DB directly (Database is imported at top of file) |
| 276 | try { |
| 277 | const ocDbPath = join(h.opencode.env.dataDir, "opencode", "opencode.db"); |
| 278 | const ocDb = openTestDb(ocDbPath, { readonly: true }); |
| 279 | // Get latest messages in the session |
| 280 | const rows = ocDb.prepare( |
| 281 | "SELECT id, json_extract(data, '$.role') AS role, json_extract(data, '$.finish') AS finish, json_extract(data, '$.summary') AS summary FROM message WHERE session_id = ? ORDER BY id DESC LIMIT 6", |
| 282 | ).all(sessionId) as Array<{ id: string; role: string; finish: string | null; summary: number | null }>; |
| 283 | const stateStr = rows |
| 284 | .map((r) => `${r.role}/${r.id.slice(0, 16)}/finish=${r.finish ?? "null"}/summary=${r.summary ?? "null"}`) |
| 285 | .join(" "); |
| 286 | console.error(`[LR-DIAG] OC-STATE turn=${turn} mockReqs=${reqs} topMsgs(newest first): ${stateStr}`); |
| 287 | |
| 288 | // Dump parts of the TOP 3 newest messages — reveals source of mystery user messages |
| 289 | // (compaction marker? autocontinue text? tool result? synthetic ignored notification?) |
| 290 | try { |
| 291 | const ocDb2 = openTestDb(ocDbPath, { readonly: true }); |
| 292 | const topIds = rows.slice(0, 3).map((r) => r.id); |
| 293 | for (const msgId of topIds) { |
| 294 | const parts = ocDb2.prepare( |
| 295 | "SELECT id, json_extract(data, '$.type') AS type, json_extract(data, '$.text') AS text, json_extract(data, '$.synthetic') AS synthetic, json_extract(data, '$.ignored') AS ignored, json_extract(data, '$.metadata') AS metadata, json_extract(data, '$.callID') AS callID, json_extract(data, '$.tool') AS tool, json_extract(data, '$.state') AS state, json_extract(data, '$.auto') AS auto, json_extract(data, '$.overflow') AS overflow FROM part WHERE message_id = ? ORDER BY id ASC LIMIT 4", |
| 296 | ).all(msgId) as Array<{ id: string; type: string; text: string | null; synthetic: number | null; ignored: number | null; metadata: string | null; callID: string | null; tool: string | null; state: string | null; auto: number | null; overflow: number | null }>; |
| 297 | const partsStr = parts.map((p) => { |
| 298 | const flags = [ |
| 299 | p.synthetic ? "synth" : null, |
| 300 | p.ignored ? "ignored" : null, |
| 301 | p.auto !== null ? `auto=${p.auto}` : null, |
| 302 | p.overflow !== null ? `overflow=${p.overflow}` : null, |
| 303 | p.callID ? `callID=${p.callID.slice(0, 12)}` : null, |
| 304 | p.tool ? `tool=${p.tool}` : null, |
| 305 | ].filter(Boolean).join(","); |
| 306 | const preview = p.text ? p.text.slice(0, 80).replace(/\n/g, "\\n") : (p.state ? `state=${p.state.slice(0, 60)}` : ""); |
| 307 | return `${p.type}${flags ? `[${flags}]` : ""}=${preview}`; |
| 308 | }).join(" || "); |
no test coverage detected