Files
pepa-pi-bot/runtime/coach/trigger-tuner.test.js
T
mayatnikovandClaude Opus 4.7 b68d4b3ee7 fix(coach/trigger-tuner): crash after ~1h — runOnce is sync, not a Promise
The live bot died overnight with:
  TypeError: runOnce(...).catch is not a function
  at trigger-tuner.js:42  →  [supervisor] child exited code=1

attach() wrapped the timer body as `runOnce().catch(...)` but
runOnce() returns a plain {ok, flagged, ...} object (pure SQL, no
await). The first tuner tick (60min after spawn) threw → killed the
whole bot process. Never surfaced before because the bot rarely ran
uninterrupted for a full hour during development.

Fix: guard the synchronous call with try/catch, matching how
persona/chatter.js already does its sync tick. (postmortem.drainOnce
and reflect.runOnce ARE async, so their .catch is correct — audited.)

Regression test added: captures the setInterval callback and invokes
it synchronously, asserting it does not throw.

Tests: 397 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 07:22:44 +03:00

120 lines
4.4 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { initKnowledge, isAvailable, listImprovements } from "../knowledge/index.js";
import { closeStore, __resetForTests } from "../knowledge/store.js";
import { runOnce, attach, detach, __testing } from "./trigger-tuner.js";
const { MIN_SAMPLE } = __testing;
test("attach: timer tick does not crash (runOnce is sync, regression for .catch bug)", async () => {
// Reproduces the crash that killed the live bot after ~1h: the
// setInterval body called runOnce().catch(...) but runOnce returns
// a plain object, not a Promise. attach must guard with try/catch.
detach();
let threw = false;
const origSetInterval = globalThis.setInterval;
let captured = null;
// capture the interval callback without actually waiting
globalThis.setInterval = (fn) => { captured = fn; return { unref() {} }; };
try {
attach({ intervalMs: 999999 });
// invoke the captured tick synchronously — must not throw
try { captured?.(); } catch { threw = true; }
} finally {
globalThis.setInterval = origSetInterval;
detach();
}
assert.equal(threw, false, "tuner timer tick must not throw");
});
async function bootstrap() {
const tmp = mkdtempSync(join(tmpdir(), "pepa-tuner-test-"));
__resetForTests();
await initKnowledge({ stateDir: tmp });
return tmp;
}
function cleanup(tmp) {
closeStore();
try { rmSync(tmp, { recursive: true, force: true }); } catch {}
}
test("runOnce: empty stats → ok with 0 flagged", async () => {
const tmp = await bootstrap();
if (!isAvailable()) { cleanup(tmp); return; }
const r = runOnce({ stats: [] });
assert.equal(r.ok, true);
assert.equal(r.flagged, 0);
cleanup(tmp);
});
test("runOnce: ignores small samples (below MIN_SAMPLE)", async () => {
const tmp = await bootstrap();
if (!isAvailable()) { cleanup(tmp); return; }
const stats = [
{ trigger_reason: "wedged_60s", total: 2, applied: 2, succeeded: 0, failed: 2, avg_in: 700, avg_out: 40, avg_latency_ms: 5000 },
];
const r = runOnce({ stats });
assert.equal(r.flagged, 0, "applied=2 is below MIN_SAMPLE; skipped");
cleanup(tmp);
});
test("runOnce: flags low success-rate trigger as improvement", async () => {
const tmp = await bootstrap();
if (!isAvailable()) { cleanup(tmp); return; }
const stats = [
{ trigger_reason: "wedged_60s", total: 10, applied: 10, succeeded: 1, failed: 9, avg_in: 700, avg_out: 40, avg_latency_ms: 5000 },
];
const r = runOnce({ stats });
assert.equal(r.flagged, 1);
const requests = listImprovements({ source: "tuner" });
assert.ok(requests.some((req) => req.title.includes("wedged_60s") && req.title.includes("low success")));
cleanup(tmp);
});
test("runOnce: flags expensive prompt with mediocre payoff", async () => {
const tmp = await bootstrap();
if (!isAvailable()) { cleanup(tmp); return; }
const stats = [
{ trigger_reason: "repeat_4_explore.far", total: 10, applied: 10, succeeded: 4, failed: 6, avg_in: 1500, avg_out: 50, avg_latency_ms: 7000 },
];
const r = runOnce({ stats });
assert.equal(r.flagged, 1);
const requests = listImprovements({ source: "tuner", category: "tuning" });
assert.ok(requests.some((req) => req.title.includes("expensive")));
cleanup(tmp);
});
test("runOnce: healthy trigger does NOT get flagged", async () => {
const tmp = await bootstrap();
if (!isAvailable()) { cleanup(tmp); return; }
const stats = [
{ trigger_reason: "emergency_hp4_creeper@3", total: 8, applied: 8, succeeded: 7, failed: 1, avg_in: 700, avg_out: 40, avg_latency_ms: 5000 },
];
const r = runOnce({ stats });
assert.equal(r.flagged, 0);
cleanup(tmp);
});
test("runOnce: re-running with same low-success stats bumps votes, not row count", async () => {
const tmp = await bootstrap();
if (!isAvailable()) { cleanup(tmp); return; }
const stats = [
{ trigger_reason: "wedged_unique_label", total: 10, applied: 10, succeeded: 1, failed: 9, avg_in: 700, avg_out: 40, avg_latency_ms: 5000 },
];
runOnce({ stats });
runOnce({ stats });
const requests = listImprovements({ source: "tuner" }).filter((r) => r.title.includes("wedged_unique_label"));
assert.equal(requests.length, 1, "single row for the same title");
assert.ok(requests[0].votes >= 2, "votes bumped on re-flagging");
cleanup(tmp);
});
test("MIN_SAMPLE constant is reasonable", () => {
assert.ok(MIN_SAMPLE >= 3 && MIN_SAMPLE <= 10);
});