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]>
This commit is contained in:
2026-09-18 07:52:35 -05:00
co-authored by Claude Fable 5.1
parent 1685deb423
commit 1949ed8d31
23 changed files with 1284 additions and 48 deletions
+27 -1
View File
@@ -204,7 +204,7 @@ test("binding: tools is optional, validated strictly, a fixed key for reload, an
const docs = join(root, "docs");
mkdirSync(docs);
const ok = validateBinding(rawBinding({ tools: { roots: [{ name: "docs", path: docs }] } }));
assert.deepEqual(ok.tools, { roots: [{ name: "docs", path: docs, write: false }], maxFileBytes: 262144, maxCallsPerTurn: 8, web: null });
assert.deepEqual(ok.tools, { roots: [{ name: "docs", path: docs, write: false, git: null }], maxFileBytes: 262144, maxCallsPerTurn: 8, web: null });
assert.ok(FIXED_KEYS.includes("tools"));
const bad = [
[{ tools: [] }, /must be an object/],
@@ -239,3 +239,29 @@ test("binding: tools is optional, validated strictly, a fixed key for reload, an
const linked = validateBinding(rawBinding({ tools: { roots: [{ name: "l", path: join(root, "docs-link") }] } }));
assert.throws(() => resolveToolRoots(linked, { dataRoot }), /symlink/);
});
test("binding: a git key is validated at load and reaches the extension whole, and only on a writable root", () => {
const root = makeRoot();
const repo = join(root, "repo");
mkdirSync(repo);
const tokenFile = join(root, "token");
writeFileSync(tokenFile, "not_a_real_token_just_a_test_value_x\n", { mode: 0o600 });
const git = { branch: "main", identity: "sage", tokenFile, author: "Sage <[email protected]>" };
const b = validateBinding(rawBinding({ tools: { roots: [{ name: "ss", path: repo, write: true, git }] } }));
assert.deepEqual(b.tools.roots[0].git, { branch: "main", identity: "sage", tokenFile, author: { name: "Sage", email: "[email protected]" }, protocol: null });
const dataRoot = join(root, "data");
mkdirSync(join(dataRoot, "discord"), { recursive: true });
assert.deepEqual(resolveToolRoots(b, { dataRoot }).roots, [{ name: "ss", path: repo, write: true, git: { branch: "main", identity: "sage", tokenFile, author: "Sage <[email protected]>" } }]);
const v = validateBinding(rawBinding({ tools: { roots: [{ name: "ss", path: repo, write: true, git: { ...git, protocol: "vault" } }] } }));
assert.equal(resolveToolRoots(v, { dataRoot }).roots[0].git.protocol, "vault");
const bad = [
[{ git }, /git needs write: true/],
[{ write: true, git: { ...git, branch: "" } }, /branch/],
[{ write: true, git: { ...git, tokenFile: join(root, "nope") } }, /not found/],
[{ write: true, git: { ...git, protocol: "x" } }, /protocol/],
[{ write: true, git: "yes" }, /git: not an object/],
];
for (const [o, re] of bad) assert.throws(() => validateBinding(rawBinding({ tools: { roots: [{ name: "ss", path: repo, ...o }] } })), re, JSON.stringify(o));
chmodSync(tokenFile, 0o640);
assert.throws(() => validateBinding(rawBinding({ tools: { roots: [{ name: "ss", path: repo, write: true, git }] } })), (e) => e instanceof DiscordError && /mode 0600/.test(e.message));
});
+1 -1
View File
@@ -29,7 +29,7 @@ test("delivery: an accepted message is in the inbox before the turn, the reply i
assert.ok(readInboxIds(journalDir).has("300000000000000001"));
assert.equal(await r.turn, "ok");
assert.equal(engine.prompts.length, 1);
assert.match(engine.prompts[0].text, /^\[discord server="Test Server" channel="#seat-admin" thread=none author=100000000000000100 message=300000000000000001\]\ntell me$/);
assert.match(engine.prompts[0].text, /^\[discord server="Test Server" channel="#seat-admin" thread=none author=100000000000000100 requester="owner" message=300000000000000001\]\ntell me$/);
assert.equal(rest.calls.length, 2);
assert.deepEqual(rest.calls.map((c) => c.nonce), ["300000000000000001-0", "300000000000000001-1"]);
assert.equal(rest.calls[0].replyTo, "300000000000000001");
+24
View File
@@ -57,6 +57,30 @@ test("context: the envelope is one bracketed line then the text; names cannot br
assert.equal(head, '[discord server="S x" channel="#c" thread="t" author=1 message=2]');
assert.deepEqual(rest, ["hi", "there"]);
assert.match(envelope({ guildName: "g", channelName: "c", authorId: "1", messageId: "2", text: "x" }), /thread=none/);
// The requester is the author's server name; quotes and brackets cannot close it early.
const r = envelope({ guildName: "g", channelName: "c", authorId: "1", requester: 'Ja "son" ]x', messageId: "2", text: "x" });
assert.equal(r.split("\n")[0], '[discord server="g" channel="#c" thread=none author=1 requester="Ja son x" message=2]');
});
test("context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol", () => {
const tokenFile = join(makeRoot(), "token");
writeFileSync(tokenFile, "not_a_real_token_just_a_test_value_x\n", { mode: 0o600 });
const git = { branch: "main", identity: "sage", tokenFile, author: "Sage <s@x>" };
const roots = [{ name: "stack-docs", path: "/r/docs", write: false }, { name: "shared-signals", path: "/r/ss", write: true, git }];
const block = discordContextBlock(binding({ tools: { roots, maxCallsPerTurn: 12 } }));
assert.match(block, /In "shared-signals" you also have git_status, git_commit, git_pull and git_push/);
assert.match(block, /naming exactly the files you changed, and it pushes at once/);
assert.match(block, /End the reply with the commit hash, and say plainly if the push failed/);
assert.match(block, /Never commit files you did not change/);
assert.doesNotMatch(block, /Jason commits it from the terminal/);
assert.doesNotMatch(block, /reserve_id/);
assert.ok(!block.includes(tokenFile), "the token path stays out of the prompt");
assert.doesNotMatch(block, /sage@|<s@x>/, "the author email stays out of the prompt");
const vault = discordContextBlock(binding({ tools: { roots: [roots[0], { ...roots[1], git: { ...git, protocol: "vault" } }], maxCallsPerTurn: 12 } }));
assert.match(vault, /"shared-signals" follows a record protocol: before creating a new record, call reserve_id/);
assert.match(vault, /commit docs\/ID-REGISTRY.txt together with the record/);
assert.match(vault, /refused while the record validator fails/);
assert.match(vault, /the author's name as this server knows them \(requester\)/);
});
test("context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable", () => {
+418
View File
@@ -0,0 +1,418 @@
// 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));
});
+3 -2
View File
@@ -255,6 +255,7 @@ test("tools: config accepts write: true only as a boolean, and enables the write
const { root, vault, c } = writeFixture();
assert.equal(c.roots[0].write, false);
assert.equal(c.roots[1].write, true);
assert.equal(c.roots[1].git, null, "no git key: the root carries git: null and no git verb is offered");
assert.deepEqual(enabledToolNames(c), ["list_dir", "read_file", "search", "write_file", "edit_file"]);
assert.throws(() => loadToolsConfig({ roots: [{ name: "v", path: vault, write: "yes" }] }), /write must be true or false/);
assert.throws(() => loadToolsConfig({ roots: [{ name: "v", path: vault, write: 1 }] }), /write must be true or false/);
@@ -308,14 +309,14 @@ test("tools: write_file leaves the exact bytes, edit_file replaces one exact mat
const { vault, c } = writeFixture();
const text = "# Names\n\n- one\n- two\n\u00e9\n";
const w = writeFile(c, { root: "vault", path: "Businesses/Names.md", text });
assert.deepEqual(w, { root: "vault", path: "Businesses/Names.md", bytes: Buffer.byteLength(text), created: true });
assert.deepEqual(w, { root: "vault", path: "Businesses/Names.md", bytes: Buffer.byteLength(text), created: true, git: false });
assert.equal(readFileSync(join(vault, "Businesses", "Names.md"), "utf8"), text);
assert.equal((lstatSync(join(vault, "Businesses", "Names.md")).mode & 0o777) <= 0o644, true);
const w2 = writeFile(c, { root: "vault", path: "Businesses/Names.md", text: "- three\n" });
assert.equal(w2.created, false);
assert.equal(readFileSync(join(vault, "Businesses", "Names.md"), "utf8"), "- three\n");
const e = editFile(c, { root: "vault", path: "Businesses/DYOR.md", old: "name: tbd", new: "name: chosen" });
assert.deepEqual(e, { root: "vault", path: "Businesses/DYOR.md", bytes: 21, created: false });
assert.deepEqual(e, { root: "vault", path: "Businesses/DYOR.md", bytes: 21, created: false, git: false });
assert.equal(readFileSync(join(vault, "Businesses", "DYOR.md"), "utf8"), "# DYOR\n\nname: chosen\n");
const set = createToolSet(c);
const r = set.call("write_file", { root: "vault", path: "Note.md", text: "hi\n" });