Baseline for the v0.2.0 self-learning iteration. All 205 tests pass on this state. Subsequent commits in this branch layer the knowledge base, post-mortem coach, and persona narration on top. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
87 lines
3.3 KiB
JavaScript
87 lines
3.3 KiB
JavaScript
// Tests for runtime/stuck-incident.js and runtime/skill-metrics.js.
|
|
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { createStuckIncidentDetector } from "./stuck-incident.js";
|
|
import { createSkillMetrics } from "./skill-metrics.js";
|
|
|
|
function snap(reason, extras = {}) {
|
|
return {
|
|
connected: true,
|
|
noProgressReason: reason,
|
|
runtimeState: "idle",
|
|
inventory: {},
|
|
curriculum: extras.curriculum ?? null,
|
|
position: { x: 0, y: 64, z: 0 },
|
|
health: 20,
|
|
food: 18,
|
|
isDay: true,
|
|
...extras,
|
|
};
|
|
}
|
|
|
|
test("returns null while reason is absent", () => {
|
|
const d = createStuckIncidentDetector({ thresholdMs: 100, cooldownMs: 1_000 });
|
|
assert.equal(d.check({ snapshot: snap(null), now: 0 }), null);
|
|
assert.equal(d.check({ snapshot: snap(null), now: 50 }), null);
|
|
});
|
|
|
|
test("does not fire before threshold elapses", () => {
|
|
const d = createStuckIncidentDetector({ thresholdMs: 100, cooldownMs: 1_000 });
|
|
assert.equal(d.check({ snapshot: snap("planner_empty"), now: 0 }), null);
|
|
assert.equal(d.check({ snapshot: snap("planner_empty"), now: 50 }), null);
|
|
assert.equal(d.check({ snapshot: snap("planner_empty"), now: 99 }), null);
|
|
});
|
|
|
|
test("fires after threshold and includes scope + body", () => {
|
|
const d = createStuckIncidentDetector({ thresholdMs: 100, cooldownMs: 1_000 });
|
|
d.check({ snapshot: snap("no_food_source"), now: 0 });
|
|
const out = d.check({
|
|
snapshot: snap("no_food_source", {
|
|
curriculum: { milestone: { title: "Secure food" }, plan: { skillId: "survive.eat" } },
|
|
}),
|
|
lastResult: { label: "survive.eat", ok: false, code: "no_food_source", detail: "no edible item" },
|
|
metrics: { "survive.eat": { ok: 0, fail: 3 } },
|
|
now: 200,
|
|
});
|
|
assert.ok(out);
|
|
assert.equal(out.fire, true);
|
|
assert.equal(out.kind, "stuck-no_food_source");
|
|
assert.match(out.summary, /no_food_source/);
|
|
assert.match(out.body, /Secure food/);
|
|
assert.match(out.body, /survive\.eat/);
|
|
assert.ok(out.editScope.some((p) => p.includes("survive-eat") || p.includes("survive.eat") || p === "runtime/skills/"));
|
|
});
|
|
|
|
test("changes in reason restart the timer (no premature fire)", () => {
|
|
const d = createStuckIncidentDetector({ thresholdMs: 100, cooldownMs: 1_000 });
|
|
d.check({ snapshot: snap("planner_empty"), now: 0 });
|
|
d.check({ snapshot: snap("planner_empty"), now: 80 });
|
|
// reason changes — restart
|
|
assert.equal(d.check({ snapshot: snap("no_food_source"), now: 100 }), null);
|
|
// only the NEW reason's clock counts now
|
|
assert.equal(d.check({ snapshot: snap("no_food_source"), now: 150 }), null);
|
|
});
|
|
|
|
test("cooldown prevents back-to-back firings", () => {
|
|
const d = createStuckIncidentDetector({ thresholdMs: 50, cooldownMs: 1_000 });
|
|
d.check({ snapshot: snap("planner_empty"), now: 0 });
|
|
const first = d.check({ snapshot: snap("planner_empty"), now: 100 });
|
|
assert.ok(first?.fire);
|
|
const second = d.check({ snapshot: snap("planner_empty"), now: 200 });
|
|
assert.equal(second, null);
|
|
});
|
|
|
|
test("skill metrics record ok/fail and expose snapshot", () => {
|
|
const m = createSkillMetrics({ persist: false });
|
|
m.record("gather.logs", true);
|
|
m.record("gather.logs", true);
|
|
m.record("gather.logs", false);
|
|
m.record("survive.eat", false);
|
|
const snap = m.snapshot();
|
|
assert.equal(snap["gather.logs"].ok, 2);
|
|
assert.equal(snap["gather.logs"].fail, 1);
|
|
assert.equal(snap["survive.eat"].fail, 1);
|
|
});
|