Files
82a8250a12 feat(auto-patch): enforce proposal editScope + run npm test before cherry-pick (#19)
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>
2026-05-26 10:39:18 +03:00

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/"]);
});