Files
stack/packages/discord/tests/git.test.mjs
T
jason.woltjeandClaude Fable 5.1 1949ed8d31 feat(discord): git verbs for the Discord Sage on the shared-signals root, seat identity through a package credential helper, vault record protocol (#1509)
Row 24. A writable root that is a git work tree may carry a git object in
the binding; the seat then has git_status, git_commit (explicit paths, seat
author, Requested-by trailer from the envelope requester, push at once per
D6), git_pull (ff-only) and git_push (one branch, never force), plus
reserve_id and per-write clone locks under protocol vault. Git children run
with no host config and one credential helper, bin/git-credential.mjs,
reading the 0600 seat token file named in the binding; the fleet helper
serves only the Gitea hosts. Suite 58/58, node 143. rev-code-02 APPROVED
round 1 (#1509 comment 26375, tree 82ab962f).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-18 07:52:35 -05:00

419 lines
23 KiB
JavaScript

// The git verbs against a local bare remote, without pi and without the
// network. Every row is a way a commit, pull or push from Discord could go
// wrong, and the fixed refusal or the honest result it gets instead. The
// vault protocol is exercised through small stand-ins for the
// shared-signals scripts that speak the same command line.
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdirSync, writeFileSync, readFileSync, chmodSync, existsSync, readdirSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { join } from "node:path";
import { loadToolsConfig, createToolSet, enabledToolNames, TOOL_NAMES, WRITE_TOOL_NAMES } from "../src/tools.mjs";
import { GIT_TOOL_NAMES, RESERVE_TOOL_NAME, GIT_REFUSAL, GitRefusal, GIT_TOKEN_FILE_ENV, GIT_USERNAME_ENV, CREDENTIAL_HELPER, VAULT_REGISTRY, gitEnv, gitStatus, gitCommit, gitPull, gitPush, reserveId, maskSecrets, parseStatus, loadGitConfig } from "../src/git.mjs";
import { makeRoot } from "./helpers.mjs";
// Built at run time so the suite's grep for token shapes never finds one
// in the source tree.
const FAKE_TOKEN = ["ghp", "_", "A".repeat(36)].join("");
function sh(cwd, args, env = {}) {
const r = spawnSync("git", args, { cwd, encoding: "utf8", env: { ...process.env, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_NOSYSTEM: "1", GIT_AUTHOR_NAME: "Jason", GIT_AUTHOR_EMAIL: "[email protected]", GIT_COMMITTER_NAME: "Jason", GIT_COMMITTER_EMAIL: "[email protected]", ...env } });
if (r.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${r.stderr}`);
return r.stdout.trim();
}
const FAKE_LOCK_TOOL = `#!/usr/bin/env python3
import os, sys, json
args = sys.argv[1:]
cmd = args.pop(0)
owner = os.environ.get("VAULT_LOCK_OWNER")
title = None
pos = []
while args:
a = args.pop(0)
if a == "--owner": owner = args.pop(0)
elif a == "--title": title = args.pop(0)
elif a == "--ttl": args.pop(0)
elif a == "--": pos.extend(args); args = []
else: pos.append(a)
root = os.getcwd()
locks = os.path.join(root, ".vault-locks")
os.makedirs(locks, exist_ok=True)
def lockfile(p): return os.path.join(locks, p.replace("/", "__") + ".json")
def holder(p):
try: return json.load(open(lockfile(p)))
except Exception: return None
if cmd == "check":
bad = False
for p in pos:
h = holder(p)
if h and h["owner"] != owner:
print(f"{p} is locked by {h['owner']} until later", file=sys.stderr); bad = True
sys.exit(1 if bad else 0)
if cmd == "lock":
p = pos[0]; h = holder(p)
if h and h["owner"] != owner: sys.exit(f"vault_lock: {p} is held by {h['owner']} until later")
json.dump({"owner": owner, "path": p}, open(lockfile(p), "w")); print(f"locked {p} for {owner}"); sys.exit(0)
if cmd == "unlock":
p = pos[0]; h = holder(p)
if h and h["owner"] == owner: os.remove(lockfile(p))
print(f"released {p}"); sys.exit(0)
if cmd == "reserve":
prefix = pos[0]
reg = os.path.join(root, "docs", "ID-REGISTRY.txt")
lines = open(reg).read().splitlines() if os.path.exists(reg) else ["# id\\tdate\\towner\\ttitle"]
nums = [int(l.split("\\t")[0].split("-")[1]) for l in lines if l.startswith(prefix + "-")]
rid = f"{prefix}-{(max(nums) + 1 if nums else 1):03d}"
lines.append(f"{rid}\\t2026-09-18T00:00:00+00:00\\t{owner}\\t{title}")
open(reg, "w").write("\\n".join(lines) + "\\n")
print(rid)
if os.environ.get("FAKE_RESERVE_NOTE"): print("note: origin/main not consulted; commit and push the registry line soon", file=sys.stderr)
sys.exit(0)
sys.exit(f"vault_lock: unknown {cmd}")
`;
const FAKE_VALIDATOR = `#!/usr/bin/env python3
import os, sys
bad = []
for dp, dn, fn in os.walk("vault"):
for f in fn:
p = os.path.join(dp, f)
if "INVALID" in open(p).read(): bad.append(p)
if bad:
for p in bad: print(f"{p}: id not registered", file=sys.stderr)
sys.exit(1)
print("PASS: structured records")
`;
// A bare remote, a clone for Sage with one commit, a second clone for
// "someone else", and a private token file. With `vault`, the stand-in
// scripts and a registry are committed in the clone.
function fixture({ vault = false } = {}) {
const base = makeRoot();
const remote = join(base, "remote.git");
const clone = join(base, "clone");
const other = join(base, "other");
mkdirSync(remote);
sh(remote, ["init", "--bare", "-q", "-b", "main"]);
sh(base, ["clone", "-q", remote, clone]);
writeFileSync(join(clone, "README.md"), "# Signals\n");
mkdirSync(join(clone, "vault"));
writeFileSync(join(clone, "vault", "SS-001.md"), "id: SS-001\n");
if (vault) {
mkdirSync(join(clone, "tools"));
mkdirSync(join(clone, "docs"));
writeFileSync(join(clone, "tools", "vault_lock.py"), FAKE_LOCK_TOOL);
writeFileSync(join(clone, "tools", "validate_vault.py"), FAKE_VALIDATOR);
writeFileSync(join(clone, "docs", "ID-REGISTRY.txt"), "# id\tdate\towner\ttitle\nSS-001\t2026-09-01T00:00:00+00:00\tjason\tfirst\n");
writeFileSync(join(clone, ".gitignore"), ".vault-locks/\n");
}
sh(clone, ["add", "-A"]);
sh(clone, ["commit", "-q", "-m", "first"]);
sh(clone, ["push", "-q", "-u", "origin", "main"]);
sh(base, ["clone", "-q", remote, other]);
const tokenFile = join(base, "token");
writeFileSync(tokenFile, `${FAKE_TOKEN}\n`, { mode: 0o600 });
const git = { branch: "main", identity: "sage", tokenFile, author: "Sage <[email protected]>", ...(vault ? { protocol: "vault" } : {}) };
const config = loadToolsConfig({ roots: [{ name: "ss", path: clone, write: true, git }], maxFileBytes: 4096, maxCallsPerTurn: 30 });
return { base, remote, clone, other, tokenFile, git, config, root: config.roots[0] };
}
const refusal = (reason) => (e) => e instanceof GitRefusal && e.reason === reason;
test("git: config validation is strict, needs write: true, a work tree and a private token file", () => {
const { clone, tokenFile, git } = fixture();
const cfg = (g, extra = {}) => loadToolsConfig({ roots: [{ name: "ss", path: clone, write: true, ...extra, git: g }], maxFileBytes: 4096, maxCallsPerTurn: 3 });
assert.equal(cfg(git).roots[0].git.author.name, "Sage");
assert.equal(cfg(git).roots[0].git.protocol, null);
assert.equal(cfg({ ...git, protocol: "vault" }).roots[0].git.protocol, "vault");
assert.throws(() => cfg(git, { write: false }), /git needs write: true/);
assert.throws(() => cfg({ ...git, extra: 1 }), /unknown key/);
assert.throws(() => cfg({ ...git, branch: "-x" }), /branch/);
assert.throws(() => cfg({ ...git, branch: "a..b" }), /branch/);
assert.throws(() => cfg({ ...git, identity: "Sage" }), /identity/);
assert.throws(() => cfg({ ...git, author: "[email protected]" }), /author/);
assert.throws(() => cfg({ ...git, protocol: "svn" }), /protocol/);
assert.throws(() => cfg({ ...git, tokenFile: "relative" }), /absolute/);
assert.throws(() => cfg({ ...git, tokenFile: join(clone, "nope") }), /not found/);
chmodSync(tokenFile, 0o644);
assert.throws(() => cfg(git), /mode 0600/);
chmodSync(tokenFile, 0o600);
const notRepo = join(clone, "vault");
assert.throws(() => loadToolsConfig({ roots: [{ name: "v", path: notRepo, write: true, git }], maxFileBytes: 4096, maxCallsPerTurn: 3 }), /not a git work tree/);
assert.throws(() => loadGitConfig(git, "x", "/nonexistent"), /not a git work tree/);
assert.deepEqual(enabledToolNames(cfg(git)), [...TOOL_NAMES, ...WRITE_TOOL_NAMES, ...GIT_TOOL_NAMES]);
assert.deepEqual(enabledToolNames(cfg({ ...git, protocol: "vault" })), [...TOOL_NAMES, ...WRITE_TOOL_NAMES, ...GIT_TOOL_NAMES, RESERVE_TOOL_NAME]);
});
test("git: the child environment drops every host git config, names one helper, and carries the token path only for origin", () => {
const { git: raw, root } = fixture();
const env = gitEnv(root.git);
assert.equal(env.GIT_CONFIG_GLOBAL, "/dev/null");
assert.equal(env.GIT_CONFIG_NOSYSTEM, "1");
assert.equal(env.GIT_TERMINAL_PROMPT, "0");
const keys = Object.entries(env).filter(([k]) => /^GIT_CONFIG_KEY_/.test(k)).map(([k, v]) => [v, env[k.replace("KEY", "VALUE")]]);
const helper = keys.find(([k]) => k === "credential.helper")[1];
assert.equal(helper, `!'${process.execPath}' '${CREDENTIAL_HELPER}'`);
assert.ok(existsSync(CREDENTIAL_HELPER));
assert.deepEqual(keys.find(([k]) => k === "user.name"), ["user.name", "Sage"]);
assert.deepEqual(keys.find(([k]) => k === "user.email"), ["user.email", "[email protected]"]);
assert.equal(env.MOSAIC_AGENT_NAME, "sage");
assert.equal(env.VAULT_LOCK_OWNER, "sage");
assert.equal(env[GIT_USERNAME_ENV], "sage");
assert.equal(env[GIT_TOKEN_FILE_ENV], undefined, "no token path for a local verb");
assert.equal(gitEnv(root.git, { remote: true })[GIT_TOKEN_FILE_ENV], raw.tokenFile);
for (const k of Object.keys(env)) assert.ok(!/TOKEN$|SECRET|PASSWORD/.test(k) || k === GIT_TOKEN_FILE_ENV, k);
});
test("git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge", () => {
const { clone, root } = fixture();
const s = gitStatus(root);
assert.equal(s.branch, "main");
assert.equal(s.upstream, "origin/main");
assert.equal(s.ahead, 0);
assert.equal(s.behind, 0);
assert.deepEqual(s.changed, []);
writeFileSync(join(clone, "README.md"), "# Signals\nmore\n");
writeFileSync(join(clone, "new.md"), "new\n");
const s2 = gitStatus(root);
assert.deepEqual(s2.changed.map((c) => c.path), ["README.md"]);
assert.deepEqual(s2.untracked, ["new.md"]);
sh(clone, ["checkout", "-q", "-b", "side"]);
assert.throws(() => gitStatus(root), refusal(GIT_REFUSAL.BRANCH));
sh(clone, ["checkout", "-q", "main"]);
sh(clone, ["checkout", "-q", "--detach"]);
assert.throws(() => gitStatus(root), refusal(GIT_REFUSAL.DETACHED));
sh(clone, ["checkout", "-q", "main"]);
writeFileSync(join(clone, ".git", "MERGE_HEAD"), `${sh(clone, ["rev-parse", "HEAD"])}\n`);
assert.throws(() => gitStatus(root), refusal(GIT_REFUSAL.IN_PROGRESS));
});
test("git: parseStatus reads porcelain v2 including renames and conflicts", () => {
const p = parseStatus([
"# branch.oid abc", "# branch.head main", "# branch.upstream origin/main", "# branch.ab +2 -1",
"1 .M N... 100644 100644 100644 abc def docs/a b.md",
"2 R. N... 100644 100644 100644 abc def R100 new name.md\told.md",
"u UU N... 100644 100644 100644 100644 a b c fight.md",
"? loose.md",
].join("\n"));
assert.equal(p.ahead, 2);
assert.equal(p.behind, 1);
assert.deepEqual(p.changed, [{ path: "docs/a b.md", state: ".M" }, { path: "new name.md", state: "R." }]);
assert.deepEqual(p.conflicts, ["fight.md"]);
assert.deepEqual(p.untracked, ["loose.md"]);
});
test("git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once", () => {
const { clone, remote, other, root } = fixture();
writeFileSync(join(clone, "vault", "SS-002.md"), "id: SS-002\n");
writeFileSync(join(clone, "vault", "SS-001.md"), "id: SS-001\nchanged\n");
writeFileSync(join(clone, "jason-draft.md"), "not sage's\n");
const r = gitCommit(root, { message: "Open SS-002 from Discord", rels: ["vault/SS-002.md", "vault/SS-001.md"], requester: "Jason" });
assert.match(r.hash, /^[0-9a-f]{7,}$/);
assert.equal(r.pushed, true);
assert.equal(r.pushError, null);
assert.deepEqual(r.paths.sort(), ["vault/SS-001.md", "vault/SS-002.md"]);
const log = sh(clone, ["log", "-1", "--format=%an%n%ae%n%cn%n%B"]);
assert.match(log, /^Sage\nsage@mosaicstack\.dev\nSage\nOpen SS-002 from Discord\n\nRequested-by: Jason/);
assert.equal(sh(remote, ["rev-parse", "main"]), sh(clone, ["rev-parse", "HEAD"]), "origin has the commit");
assert.deepEqual(sh(clone, ["status", "--porcelain"]).split("\n"), ["?? jason-draft.md"], "Jason's own file is untouched and not committed");
sh(other, ["pull", "-q", "--ff-only"]);
assert.ok(existsSync(join(other, "vault", "SS-002.md")));
});
test("git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work", () => {
const { clone, root, config } = fixture();
const ok = { message: "m", rels: ["README.md"], requester: "Jason" };
assert.throws(() => gitCommit(root, { ...ok, message: "" }), refusal(GIT_REFUSAL.BAD_MESSAGE));
assert.throws(() => gitCommit(root, { ...ok, message: "x".repeat(501) }), refusal(GIT_REFUSAL.BAD_MESSAGE));
assert.throws(() => gitCommit(root, { ...ok, rels: [] }), refusal(GIT_REFUSAL.BAD_PATHS));
assert.throws(() => gitCommit(root, { ...ok, rels: ["--all"] }), refusal(GIT_REFUSAL.BAD_PATHS));
assert.throws(() => gitCommit(root, { ...ok, requester: null }), refusal(GIT_REFUSAL.NO_REQUESTER));
assert.throws(() => gitCommit(root, { ...ok, requester: "a\nb" }), refusal(GIT_REFUSAL.NO_REQUESTER));
assert.throws(() => gitCommit(root, ok), refusal(GIT_REFUSAL.NOTHING), "unchanged file");
assert.equal(sh(clone, ["diff", "--cached", "--name-only"]), "", "nothing left staged after the refusal");
writeFileSync(join(clone, "README.md"), "# Signals\nmore\n");
writeFileSync(join(clone, "staged-by-jason.md"), "x\n");
sh(clone, ["add", "staged-by-jason.md"]);
assert.throws(() => gitCommit(root, ok), refusal(GIT_REFUSAL.INDEX_DIRTY));
sh(clone, ["reset", "-q"]);
// Through the tool set: the path fence and the requester come from state.
const tools = createToolSet(config);
const r1 = tools.call("git_commit", { root: "ss", message: "m", paths: ["README.md"] });
assert.equal(r1.ok, false);
assert.match(r1.text, /requester of this message is unknown/);
tools.setRequester("Jason");
for (const paths of [["../x"], [".git/config"], ["vault"], ["missing.md"], [42]]) {
const r = tools.call("git_commit", { root: "ss", message: "m", paths });
assert.equal(r.ok, false, JSON.stringify(paths));
}
assert.equal(sh(clone, ["rev-list", "--count", "HEAD"]), "1", "no commit happened");
const r2 = tools.call("git_commit", { root: "ss", message: "Explain the signals", paths: ["README.md", "README.md"] });
assert.equal(r2.ok, true, r2.text);
assert.match(r2.text, /committed [0-9a-f]+ on main for Jason: README.md; pushed to origin/);
assert.equal(r2.details.pushed, true);
assert.equal(r2.details.requester, "Jason");
assert.deepEqual(r2.details.paths, ["README.md"]);
assert.equal(sh(clone, ["rev-list", "--count", "HEAD"]), "2");
const r3 = tools.call("git_commit", { root: "stack-docs", message: "m", paths: ["README.md"] });
assert.match(r3.text, /unknown root/);
});
test("git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6)", () => {
const { clone, remote, root } = fixture();
sh(clone, ["remote", "set-url", "origin", join(clone, "..", "nowhere.git")]);
writeFileSync(join(clone, "a.md"), "a\n");
const r1 = gitCommit(root, { message: "a", rels: ["a.md"], requester: "Carmen" });
assert.equal(r1.pushed, false);
assert.match(r1.pushError, /could not push/);
assert.equal(sh(clone, ["rev-list", "--count", "HEAD"]), "2");
sh(clone, ["remote", "set-url", "origin", remote]);
writeFileSync(join(clone, "b.md"), "b\n");
const r2 = gitCommit(root, { message: "b", rels: ["b.md"], requester: "Carmen" });
assert.equal(r2.pushed, true);
assert.equal(sh(remote, ["rev-parse", "main"]), sh(clone, ["rev-parse", "HEAD"]));
assert.equal(sh(remote, ["rev-list", "--count", "main"]), "3");
});
test("git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged", () => {
const { clone, other, root } = fixture();
assert.equal(gitPull(root).updated, false);
writeFileSync(join(other, "from-other.md"), "o\n");
sh(other, ["add", "from-other.md"]);
sh(other, ["commit", "-q", "-m", "other"]);
sh(other, ["push", "-q", "origin", "main"]);
const p = gitPull(root);
assert.equal(p.updated, true);
assert.notEqual(p.from, p.to);
assert.ok(existsSync(join(clone, "from-other.md")));
// Diverged: a local commit and a different remote commit.
writeFileSync(join(clone, "local.md"), "l\n");
sh(clone, ["add", "local.md"]);
sh(clone, ["commit", "-q", "-m", "local"]);
writeFileSync(join(other, "from-other.md"), "o2\n");
sh(other, ["commit", "-q", "-am", "other2"]);
sh(other, ["push", "-q", "origin", "main"]);
assert.throws(() => gitPull(root), refusal(GIT_REFUSAL.NON_FF));
assert.equal(sh(clone, ["rev-list", "--count", "HEAD"]), "3", "nothing merged");
assert.throws(() => gitPush(root), refusal(GIT_REFUSAL.PUSH_FAILED), "a diverged push is rejected, never forced");
sh(clone, ["reset", "-q", "--hard", "origin/main"]);
sh(clone, ["pull", "-q", "--ff-only"]);
// Dirty: origin changes a file that has local edits.
writeFileSync(join(other, "from-other.md"), "o3\n");
sh(other, ["commit", "-q", "-am", "other3"]);
sh(other, ["push", "-q", "origin", "main"]);
writeFileSync(join(clone, "from-other.md"), "mine\n");
assert.throws(() => gitPull(root), refusal(GIT_REFUSAL.DIRTY));
assert.equal(readFileSync(join(clone, "from-other.md"), "utf8"), "mine\n", "the local edit survives");
});
test("git: push pushes the named branch only and reports up to date", () => {
const { clone, remote, root } = fixture();
assert.equal(gitPush(root).upToDate, true);
writeFileSync(join(clone, "c.md"), "c\n");
sh(clone, ["add", "c.md"]);
sh(clone, ["commit", "-q", "-m", "c"]);
const r = gitPush(root);
assert.equal(r.upToDate, false);
assert.equal(r.pushed, true);
assert.equal(sh(remote, ["rev-parse", "main"]), sh(clone, ["rev-parse", "HEAD"]));
});
test("git: no token value or token path ever reaches a git argument list; outputs are masked and capped", () => {
const { clone, root, tokenFile } = fixture();
const argvs = [];
const spy = (cmd, args, opts) => {
argvs.push([cmd, ...args]);
return spawnSync(cmd, args, opts);
};
writeFileSync(join(clone, "d.md"), "d\n");
gitCommit(root, { message: "d", rels: ["d.md"], requester: "Jason" }, { spawn: spy });
gitPull(root, { spawn: spy });
gitPush(root, { spawn: spy });
gitStatus(root, { spawn: spy });
assert.ok(argvs.length > 5);
for (const argv of argvs) {
const joined = argv.join(" ");
assert.ok(!joined.includes(FAKE_TOKEN), joined);
assert.ok(!joined.includes(tokenFile), joined);
assert.ok(!/--force|-f\b|--tags|--all/.test(joined), joined);
}
assert.equal(maskSecrets(`fatal: unable to access 'https://sage:${FAKE_TOKEN}@github.com/x/y': 401`), "fatal: unable to access 'https://<masked>@github.com/x/y': 401");
assert.equal(maskSecrets(`remote: ${FAKE_TOKEN} was used`), "remote: <masked> was used");
assert.equal(maskSecrets(`github_pat_${"B".repeat(40)} x`), "<masked> x");
});
test("git: the credential helper answers get over https from a private file and nothing else", () => {
const { tokenFile } = fixture();
const call = (action, input, env) => spawnSync(process.execPath, [CREDENTIAL_HELPER, action], { input, encoding: "utf8", env: { PATH: process.env.PATH, [GIT_TOKEN_FILE_ENV]: tokenFile, [GIT_USERNAME_ENV]: "sage", ...env } });
const ok = call("get", "protocol=https\nhost=github.com\n\n");
assert.equal(ok.status, 0, ok.stderr);
assert.equal(ok.stdout, `username=sage\npassword=${FAKE_TOKEN}\n`);
assert.equal(call("store", "protocol=https\nhost=github.com\n\n").stdout, "");
assert.equal(call("erase", "protocol=https\nhost=github.com\n\n").status, 0);
const http = call("get", "protocol=http\nhost=github.com\n\n");
assert.equal(http.status, 1);
assert.match(http.stderr, /only https/);
assert.ok(!http.stderr.includes(FAKE_TOKEN));
const noFile = call("get", "protocol=https\nhost=github.com\n\n", { [GIT_TOKEN_FILE_ENV]: "" });
assert.equal(noFile.status, 1);
chmodSync(tokenFile, 0o644);
const loose = call("get", "protocol=https\nhost=github.com\n\n");
assert.equal(loose.status, 1);
assert.match(loose.stderr, /mode 0600/);
assert.equal(loose.stdout, "");
chmodSync(tokenFile, 0o600);
writeFileSync(tokenFile, "short\n", { mode: 0o600 });
const bad = call("get", "protocol=https\nhost=github.com\n\n");
assert.equal(bad.status, 1);
assert.equal(bad.stdout, "");
});
test("git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes", () => {
const { clone, root, config } = fixture({ vault: true });
const tools = createToolSet(config);
tools.setRequester("Jason");
assert.deepEqual(enabledToolNames(config).slice(-5), [...GIT_TOOL_NAMES, RESERVE_TOOL_NAME]);
// reserve_id appends the registry line and returns the id.
const bad = tools.call("reserve_id", { root: "ss", prefix: "XX", title: "t" });
assert.match(bad.text, /prefix must be one of/);
assert.match(tools.call("reserve_id", { root: "ss", prefix: "SS", title: "a\nb" }).text, /title must be/);
const res = tools.call("reserve_id", { root: "ss", prefix: "SS", title: "Maestro naming" });
assert.equal(res.ok, true, res.text);
assert.equal(res.details.id, "SS-002");
assert.match(res.text, /reserved SS-002; the registry line is in docs\/ID-REGISTRY.txt/);
assert.match(readFileSync(join(clone, VAULT_REGISTRY), "utf8"), /SS-002\t[^\t]+\tsage\tMaestro naming\n$/);
// A record that fails the validator refuses the commit; nothing staged.
const w = tools.call("write_file", { root: "ss", path: "vault/SS-002.md", text: "id: SS-002\nINVALID\n" });
assert.equal(w.ok, true, w.text);
assert.match(w.text, /commit it with git_commit/);
assert.deepEqual(readdirSync(join(clone, ".vault-locks")), [], "the write's lock was released");
const c1 = tools.call("git_commit", { root: "ss", message: "Add SS-002", paths: ["vault/SS-002.md", VAULT_REGISTRY] });
assert.equal(c1.ok, false);
assert.match(c1.text, /record validator failed/);
assert.match(c1.text, /SS-002.md: id not registered/);
assert.equal(sh(clone, ["diff", "--cached", "--name-only"]), "");
// Fixed record: the commit goes through with both paths and pushes.
tools.call("write_file", { root: "ss", path: "vault/SS-002.md", text: "id: SS-002\ntitle: Maestro naming\n" });
const c2 = tools.call("git_commit", { root: "ss", message: "Add SS-002", paths: ["vault/SS-002.md", VAULT_REGISTRY] });
assert.equal(c2.ok, true, c2.text);
assert.equal(c2.details.pushed, true);
assert.deepEqual(sh(clone, ["show", "--name-only", "--format=", "HEAD"]).split("\n").sort(), [VAULT_REGISTRY, "vault/SS-002.md"]);
// Another owner's lock refuses a write and a commit that touches the path.
mkdirSync(join(clone, ".vault-locks"), { recursive: true });
writeFileSync(join(clone, ".vault-locks", "vault__SS-002.md.json"), JSON.stringify({ owner: "jason", path: "vault/SS-002.md" }));
const w2 = tools.call("edit_file", { root: "ss", path: "vault/SS-002.md", old: "naming", new: "name" });
assert.equal(w2.ok, false);
assert.match(w2.text, /locked by another contributor: vault_lock: vault\/SS-002.md is held by jason/);
assert.match(readFileSync(join(clone, "vault", "SS-002.md"), "utf8"), /naming/, "the file is untouched");
writeFileSync(join(clone, "vault", "SS-002.md"), "id: SS-002\ntitle: Maestro name\n");
const c3 = tools.call("git_commit", { root: "ss", message: "rename", paths: ["vault/SS-002.md"] });
assert.equal(c3.ok, false);
assert.match(c3.text, /locked by another contributor: vault\/SS-002.md is locked by jason/);
assert.equal(sh(clone, ["diff", "--cached", "--name-only"]), "");
// Without the protocol, reserve_id is not offered and writes take no lock.
const plain = fixture();
assert.ok(!enabledToolNames(plain.config).includes(RESERVE_TOOL_NAME));
assert.throws(() => reserveId(plain.root, { prefix: "SS", title: "t" }), refusal(GIT_REFUSAL.NO_PROTOCOL));
});