Files
pepa-pi-bot/runtime/modes.test.js
T
mayatnikovandClaude Opus 4.7 4ae63dabe1 feat(runtime): v0.1.0 — adopt Voyager critic + Mindcraft modes/library/lint
Five concrete patterns from Voyager and Mindcraft, applied in our shape
without abandoning the git-as-evolution-substrate that makes pepa
distinct. Plus a first multi-agent surface so two bots from the same
repo can share intent.

1. runtime/critic.js (Voyager critic.txt)
   - Spawns `pi -p` with a JSON-only critic prompt before a proposal is
     written. {reasoning, success, critique}.
   - success=true short-circuits the proposal (bot recovered between
     detector tripping and now), saving Pi tokens on false positives.
   - critique is spliced into the proposal body via attachCritique() so
     the downstream auto-patcher has a sharp spec.
   - Graceful: pi missing / timeout / unparseable JSON → proposal still
     filed without the critic block.

2. scripts/lint-patch.js (Mindcraft coder._lintCode)
   - Pre-flight gate between Pi commit and npm test: node --check, dynamic
     import (catches missing named exports), regex extraction of
     runSkill("id") calls cross-checked against the live registry.
   - Cheaper than npm test, fails fast with a clear reason.

3. runtime/stuck-incident.renderActionTemplate (Voyager action_template.txt)
   - All proposal bodies now follow the same fixed-section layout: Task /
     Last result / Execution error / State / Metrics / Journal /
     Scenarios / Critique / Fix / Edit scope / Forbidden.

4. runtime/skill-library.js (Mindcraft skill_library.getRelevantSkillDocs)
   - Word-overlap ranking (Mindcraft's offline fallback) — zero deps,
     deterministic. auto-patch.js injects top-3 similar skills into the
     Pi prompt as "look at these patterns".

5. runtime/modes.js (Mindcraft modes.js)
   - Declarative {name, interrupts, on, active, update(ctx)} chain that
     runs BEFORE the curriculum each tick.
   - Ships self_preservation (low HP → eat/flee), hunger (food<14 → eat),
     night_shelter (night + bed in hand → sleep). Cleaner than ad-hoc
     lastFleeAttempt cooldowns in reflex.js.

6. runtime/social/conversation.js + cmd:conv-say/conv-recent/conv-list
   - File-JSONL topic channel so two bots from the same repo (different
     usernames, different host dirs under state/) can append turns and
     read peers. Skeleton — multi-agent collaboration on top later.

Differentiator preserved: every Pi-written skill still lands on main via
auto-patch.js (real git branch + smoke gate + cherry-pick). Voyager
keeps skills in a Chroma JSON, Mindcraft keeps them in RAM — pepa keeps
them as versioned source code reviewable in `git log`.

package.json: 0.0.1 → 0.1.0. 174/174 tests pass. README + AGENTS updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:16:42 +03:00

96 lines
3.2 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { registerMode, tickModes, listModes, setModeEnabled, _resetModes } from "./modes.js";
test("tickModes: first mode that fires wins", async () => {
_resetModes();
registerMode({
name: "high",
interrupts: ["all"],
update: () => ({ action: { skillId: "from-high" } }),
});
registerMode({
name: "low",
update: () => ({ action: { skillId: "from-low" } }),
});
const out = tickModes({});
assert.equal(out.mode, "high");
assert.equal(out.action.skillId, "from-high");
});
test("tickModes: disabled mode is skipped", async () => {
_resetModes();
registerMode({ name: "skipme", update: () => ({ action: { skillId: "x" } }) });
registerMode({ name: "use", update: () => ({ action: { skillId: "y" } }) });
setModeEnabled("skipme", false);
const out = tickModes({});
assert.equal(out.mode, "use");
});
test("tickModes: returns null when no mode fires", async () => {
_resetModes();
registerMode({ name: "silent", update: () => null });
const out = tickModes({});
assert.equal(out, null);
});
test("tickModes: thrown update doesn't break the chain", async () => {
_resetModes();
registerMode({
name: "throws",
update: () => { throw new Error("boom"); },
});
registerMode({
name: "next",
update: () => ({ action: { skillId: "rescued" } }),
});
const out = tickModes({});
assert.equal(out.mode, "next");
});
test("registerMode: same name replaces, doesn't duplicate", async () => {
_resetModes();
registerMode({ name: "x", update: () => ({ action: { skillId: "v1" } }) });
registerMode({ name: "x", update: () => ({ action: { skillId: "v2" } }) });
const out = tickModes({});
assert.equal(out.action.skillId, "v2");
assert.equal(listModes().length, 1);
});
test("standard modes load on import", async () => {
const mod = await import(`./modes.js?cb=${Date.now()}`);
const names = mod.listModes().map((m) => m.name);
assert.ok(names.includes("self_preservation"));
assert.ok(names.includes("hunger"));
assert.ok(names.includes("night_shelter"));
});
test("self_preservation: low-HP + food + hasFood → eat", async () => {
_resetModes();
const mod = await import(`./modes.js?cb=${Date.now() + 1}`);
const out = mod.tickModes({ snapshot: { health: 4, food: 10, hasFood: true } });
assert.equal(out.mode, "self_preservation");
assert.equal(out.action.skillId, "eat");
});
test("hunger: food below 14 with food → eat", async () => {
_resetModes();
const mod = await import(`./modes.js?cb=${Date.now() + 2}`);
const out = mod.tickModes({ snapshot: { health: 20, food: 12, hasFood: true } });
assert.equal(out.action.skillId, "eat");
});
test("night_shelter: day → null (skip)", async () => {
_resetModes();
const mod = await import(`./modes.js?cb=${Date.now() + 3}`);
const out = mod.tickModes({ snapshot: { isDay: true, food: 20, hasFood: false, inventory: { red_bed: 1 } } });
assert.equal(out, null);
});
test("night_shelter: night + bed in hand → sleep", async () => {
_resetModes();
const mod = await import(`./modes.js?cb=${Date.now() + 4}`);
const out = mod.tickModes({ snapshot: { isDay: false, food: 20, hasFood: false, inventory: { red_bed: 1 } } });
assert.equal(out.action.skillId, "sleep");
});