Two safety rails on the unattended self-improvement loop:
1. scripts/edit-scope.js + .test.js: pure helpers that parse
`editScope: [...]` out of a proposal frontmatter (the field Phase 6
started writing) and validate a list of changed files against it.
13 tests covering null/missing/malformed frontmatter, directory
prefix matching, exact-file matching, default-scope fallback.
2. scripts/auto-patch.js:
- reads editScope from the proposal (falls back to ["runtime/"])
- injects the allowed paths into the Pi prompt so Pi knows the
boundaries up front
- validates the diff against scope + auto-allows any
runtime/**/*.test.js files Pi added
- runs `npm test` on the patched branch BEFORE cherry-picking;
refuses to land a patch that breaks the suite
Closes the "Smoke checks run before applying patch" item from
plans/autonomous-survival-bot-prd.md §7 Phase 6. npm test now 92/92.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
83 lines
3.1 KiB
JavaScript
83 lines
3.1 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { parseEditScope, isFileInScope, validateChangedFiles, effectiveScope } from "./edit-scope.js";
|
|
|
|
test("parseEditScope returns null when no frontmatter present", () => {
|
|
assert.equal(parseEditScope(""), null);
|
|
assert.equal(parseEditScope("# no frontmatter\nbody"), null);
|
|
});
|
|
|
|
test("parseEditScope returns null when frontmatter has no editScope key", () => {
|
|
const text = "---\nkind: test\napproved: false\n---\nbody";
|
|
assert.equal(parseEditScope(text), null);
|
|
});
|
|
|
|
test("parseEditScope reads a JSON array", () => {
|
|
const text = '---\nkind: test\neditScope: ["runtime/skills/eat.js","runtime/skills/"]\n---\nbody';
|
|
assert.deepEqual(parseEditScope(text), ["runtime/skills/eat.js", "runtime/skills/"]);
|
|
});
|
|
|
|
test("parseEditScope strips leading ./ and / from entries", () => {
|
|
const text = '---\neditScope: ["./runtime/", "/scripts/edit-scope.js"]\n---\n';
|
|
assert.deepEqual(parseEditScope(text), ["runtime/", "scripts/edit-scope.js"]);
|
|
});
|
|
|
|
test("parseEditScope returns null for malformed JSON", () => {
|
|
const text = "---\neditScope: not-json\n---\n";
|
|
assert.equal(parseEditScope(text), null);
|
|
});
|
|
|
|
test("isFileInScope: directory prefix matches descendants", () => {
|
|
assert.equal(isFileInScope("runtime/skills/eat.js", ["runtime/skills/"]), true);
|
|
assert.equal(isFileInScope("runtime/bot.js", ["runtime/skills/"]), false);
|
|
});
|
|
|
|
test("isFileInScope: bare directory name matches as prefix", () => {
|
|
// "runtime/skills" should still cover "runtime/skills/foo.js"
|
|
assert.equal(isFileInScope("runtime/skills/foo.js", ["runtime/skills"]), true);
|
|
// but not "runtime/skillsX/foo.js"
|
|
assert.equal(isFileInScope("runtime/skillsX/foo.js", ["runtime/skills"]), false);
|
|
});
|
|
|
|
test("isFileInScope: exact file match", () => {
|
|
assert.equal(isFileInScope("runtime/bot.js", ["runtime/bot.js"]), true);
|
|
assert.equal(isFileInScope("runtime/bot.js.bak", ["runtime/bot.js"]), false);
|
|
});
|
|
|
|
test("validateChangedFiles: all in-scope → ok", () => {
|
|
const out = validateChangedFiles(
|
|
["runtime/skills/eat.js", "runtime/skills/groups.js"],
|
|
["runtime/skills/"],
|
|
);
|
|
assert.equal(out.ok, true);
|
|
assert.deepEqual(out.outsideFiles, []);
|
|
});
|
|
|
|
test("validateChangedFiles: any out-of-scope → not ok, list returned", () => {
|
|
const out = validateChangedFiles(
|
|
["runtime/skills/eat.js", "package.json", ".env"],
|
|
["runtime/skills/"],
|
|
);
|
|
assert.equal(out.ok, false);
|
|
assert.deepEqual(out.outsideFiles.sort(), [".env", "package.json"]);
|
|
});
|
|
|
|
test("validateChangedFiles: falls back to default scope when empty", () => {
|
|
const out = validateChangedFiles(["runtime/bot.js"], null);
|
|
assert.equal(out.ok, true);
|
|
assert.deepEqual(out.effectiveScope, ["runtime/"]);
|
|
});
|
|
|
|
test("validateChangedFiles: default scope rejects scripts/", () => {
|
|
const out = validateChangedFiles(["scripts/auto-patch.js"], []);
|
|
assert.equal(out.ok, false);
|
|
assert.deepEqual(out.outsideFiles, ["scripts/auto-patch.js"]);
|
|
});
|
|
|
|
test("effectiveScope mirrors fallback", () => {
|
|
assert.deepEqual(effectiveScope(null), ["runtime/"]);
|
|
assert.deepEqual(effectiveScope([]), ["runtime/"]);
|
|
assert.deepEqual(effectiveScope(["x/"]), ["x/"]);
|
|
});
|