review loop: working repositories, git socket, push and review verdicts, row 93 (#1587)
Commits move as v2 bundles over bus-host/git.sock (decision 135). The bus
host keeps one bare working repository per business and project, seeds main
from the project root under a per-repository lock, fetches a pushed bundle
with fsck into a temp ref, requires history shared with main, and moves the
task's work ref only as a fast-forward. Host and runner git run with no
system or global config, hooks at /dev/null and local transports only. The
socket binds only for roles holding git.push.working or review.verdict.
The broker records a push as the action.allowed for git.push.working with
its commit, takes review.request for the latest push and one review.verdict
per request, and refuses task.close on a pushed task without an approve
verdict for the latest push. recordEvent reserves both shapes, so agents
can't forge either.
Reviewed by Dewey (28196) and Sage (28197), both approve. Gate on 7a729aef:
14 node suites and 9 scripts green.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
// Row 93 (#1587): the host's git socket inside a real bus host, driven by the
|
||||
// runner's own git tools (packages/harness/src/git-tools.mjs) over real
|
||||
// clones. A coder pushes, asks for a review, the reviewer fetches the commit
|
||||
// and answers, all as the two roles' launches. No sandbox: the socket and the
|
||||
// tools are what is under test, and each test checks that the project root is
|
||||
// unchanged and that no bundle file is left in bus-host/ or either tmp.
|
||||
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { connect } from "node:net";
|
||||
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Client } from "../../bus/src/client.mjs";
|
||||
import { configDir, loadBusiness } from "../../business/src/index.mjs";
|
||||
import { gitEnv } from "../../seat/src/checkout.mjs";
|
||||
import { gitSocketPath } from "../../seat/src/session.mjs";
|
||||
import { workingRepo } from "../../seat/src/working.mjs";
|
||||
import { fetchReview, pushWorking } from "../../harness/src/git-tools.mjs";
|
||||
import { bootConfig, loadSystem, rolesDir } from "../src/config.mjs";
|
||||
import { createGitSocket } from "../src/git-socket.mjs";
|
||||
import { startHost, startTimeOf } from "../src/host.mjs";
|
||||
import { fixture } from "./helpers.mjs";
|
||||
|
||||
const TASK = "vikunja:1/2";
|
||||
|
||||
function gitIn(cwd, ...args) {
|
||||
const r = spawnSync("git", ["-c", "user.name=test", "-c", "[email protected]", "-C", cwd, ...args], { env: gitEnv(), encoding: "utf8" });
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
return r.stdout.trim();
|
||||
}
|
||||
function commit(dir, text, file = "file.txt") {
|
||||
writeFileSync(join(dir, file), `${text}\n`);
|
||||
gitIn(dir, "add", file);
|
||||
gitIn(dir, "commit", "-q", "-m", text);
|
||||
return gitIn(dir, "rev-parse", "HEAD");
|
||||
}
|
||||
function fingerprint(dir) {
|
||||
const h = createHash("sha256");
|
||||
const walk = (d) => {
|
||||
for (const name of readdirSync(d).sort()) {
|
||||
const p = join(d, name);
|
||||
const st = lstatSync(p);
|
||||
h.update(`${p}\0${st.mode}\0`);
|
||||
if (st.isDirectory()) walk(p);
|
||||
else if (st.isSymbolicLink()) h.update(readlinkSync(p));
|
||||
else if (st.isFile()) h.update(readFileSync(p));
|
||||
}
|
||||
};
|
||||
walk(dir);
|
||||
return h.digest("hex");
|
||||
}
|
||||
const bundles = (dir) => (existsSync(dir) ? readdirSync(dir).filter((n) => n.endsWith(".bundle")) : []);
|
||||
|
||||
// A host for business acme, its git socket, and the coder, reviewer and pm
|
||||
// bound and claimed. Each role has a workspace with a clone of the project.
|
||||
async function setup(t, git = {}) {
|
||||
// One teardown, in order: stop the socket and the host, check, then remove
|
||||
// the tree. (t.after hooks run first-registered first.)
|
||||
const root = mkdtempSync(join(tmpdir(), "mosaic-git-"));
|
||||
const down = [];
|
||||
t.after(async () => {
|
||||
try {
|
||||
for (const fn of down) await fn();
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
const f = fixture(root, "acme");
|
||||
mkdirSync(f.dataRoot, { mode: 0o700 });
|
||||
const project = f.doc.projects.stack.root;
|
||||
gitIn(project, "init", "-q", "-b", "main");
|
||||
const main = commit(project, "one");
|
||||
const before = fingerprint(project);
|
||||
const system = loadSystem({ env: f.env });
|
||||
const host = await startHost({ boot: bootConfig({ system, businessId: "acme", env: f.env }), business: "acme", log: () => {} });
|
||||
down.push(() => host.close(0));
|
||||
const logs = [];
|
||||
const sock = createGitSocket({ host, business: loadBusiness("acme", { dir: configDir(f.env), rolesDir: rolesDir(f.env) }), dataRoot: f.dataRoot, env: f.env, log: (l) => logs.push(l), ...git });
|
||||
down.unshift(() => sock.close());
|
||||
await sock.listen();
|
||||
const roles = {};
|
||||
for (const role of ["coder", "reviewer", "pm", "cto"]) {
|
||||
const { cap } = await host.bindLaunch({ business: "acme", role, run: `${role}-test`, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) });
|
||||
const client = new Client({ path: host.path, cap });
|
||||
await client.call("role.claim");
|
||||
const workspace = join(root, "ws", role);
|
||||
const tmpDir = join(root, "tmp", role);
|
||||
mkdirSync(workspace, { recursive: true });
|
||||
mkdirSync(tmpDir, { recursive: true });
|
||||
gitIn(workspace, "clone", "-q", project, "stack");
|
||||
roles[role] = { cap, client, workspace, tmp: tmpDir, clone: join(workspace, "stack") };
|
||||
}
|
||||
// Checked after every test: the root as it was, and no bundle anywhere.
|
||||
down.push(() => {
|
||||
assert.equal(fingerprint(project), before, "the project root is unchanged");
|
||||
for (const dir of [join(f.dataRoot, "bus-host"), ...Object.values(roles).map((r) => r.tmp)]) assert.deepEqual(bundles(dir), [], `no bundle left in ${dir}`);
|
||||
});
|
||||
const push = (role, args = {}) => pushWorking({ path: sock.path, cap: roles[role].cap, workspace: roles[role].workspace, tmp: roles[role].tmp, task_ref: TASK, project: "stack", branch: "feat", ...args });
|
||||
const fetch = (role, request, args = {}) => fetchReview({ path: sock.path, cap: roles[role].cap, workspace: roles[role].workspace, tmp: roles[role].tmp, task_ref: TASK, request, ...args });
|
||||
const repo = workingRepo(f.dataRoot, "acme", "stack");
|
||||
const refs = () => (existsSync(repo) ? gitIn(root, `--git-dir=${repo}`, "for-each-ref", "--format=%(refname) %(objectname)") : "");
|
||||
const trail = async (kind, subject = TASK) => (await roles.cto.client.call("trail", { subject })).filter((r) => r.table === "events" && r.kind === kind);
|
||||
return { root, f, project, main, host, sock, roles, push, fetch, repo, refs, trail, logs };
|
||||
}
|
||||
|
||||
const rejects = (p, code) => assert.rejects(p, (e) => e.code === code || assert.fail(`${e.code}: ${e.message}, wanted ${code}`));
|
||||
|
||||
// One raw exchange on the git socket.
|
||||
function raw(path, text) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const s = connect(path);
|
||||
let buf = "";
|
||||
s.on("connect", () => s.write(text));
|
||||
s.on("data", (b) => (buf += b));
|
||||
s.on("end", () => resolve(JSON.parse(buf.split("\n")[0])));
|
||||
s.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
test("the review loop: the coder pushes and asks, the reviewer fetches and approves, and the task's trail holds each step", async (t) => {
|
||||
const w = await setup(t);
|
||||
assert.equal(statSync(w.sock.path).mode & 0o777, 0o600);
|
||||
assert.equal(w.sock.path, gitSocketPath(w.f.dataRoot));
|
||||
const { coder, reviewer } = w.roles;
|
||||
gitIn(coder.clone, "checkout", "-q", "-b", "feat");
|
||||
const c1 = commit(coder.clone, "two");
|
||||
assert.deepEqual(await w.push("coder"), { ref: "work/vikunja-1-2", commit: c1, previous: null });
|
||||
assert.equal(gitIn(coder.clone, "rev-parse", "refs/remotes/working/work/vikunja-1-2"), c1);
|
||||
assert.equal(w.refs(), `refs/heads/main ${w.main}\nrefs/heads/work/vikunja-1-2 ${c1}`);
|
||||
// Nothing new: the clone remembers the push.
|
||||
await rejects(w.push("coder"), "nothing-to-push");
|
||||
|
||||
const req = await coder.client.call("review.request", { task_ref: TASK, commit: c1, to: "reviewer" });
|
||||
const got = await w.fetch("reviewer", req.request);
|
||||
assert.deepEqual(got, { project: "stack", ref: "working/work/vikunja-1-2", commit: c1, clone: reviewer.clone });
|
||||
assert.equal(gitIn(reviewer.clone, "rev-parse", "refs/remotes/working/work/vikunja-1-2"), c1);
|
||||
assert.equal(gitIn(reviewer.clone, "show", "-s", "--format=%s", c1), "two");
|
||||
const v = await reviewer.client.call("review.verdict", { task_ref: TASK, request: req.request, verdict: "changes", body: "one more" });
|
||||
assert.equal(v.commit, c1);
|
||||
|
||||
// The coder answers with a fast-forward; the bundle carries only the new commit.
|
||||
const c2 = commit(coder.clone, "three");
|
||||
assert.deepEqual(await w.push("coder"), { ref: "work/vikunja-1-2", commit: c2, previous: c1 });
|
||||
const req2 = await coder.client.call("review.request", { task_ref: TASK, commit: c2, to: "reviewer" });
|
||||
assert.equal((await w.fetch("reviewer", req2.request)).commit, c2);
|
||||
const ok = await reviewer.client.call("review.verdict", { task_ref: TASK, request: req2.request, verdict: "approve", body: "good" });
|
||||
|
||||
// A push is the action.allowed for git.push.working that names its commit.
|
||||
const pushes = (await w.trail("action.allowed")).filter((e) => e.body.action === "git.push.working");
|
||||
assert.deepEqual(pushes.map((e) => [e.actor_role, e.body.commit, e.body.previous, e.body.branch]), [["coder", c1, null, "feat"], ["coder", c2, c1, "feat"]]);
|
||||
assert.deepEqual((await w.trail("review.requested")).map((e) => e.id), [req.request, req2.request]);
|
||||
assert.deepEqual((await w.trail("review.verdict")).map((e) => [e.id, e.body.verdict, e.body.commit]), [[v.verdict, "changes", c1], [ok.verdict, "approve", c2]]);
|
||||
assert.deepEqual(w.logs, []);
|
||||
});
|
||||
|
||||
test("refusals: no action, non-fast-forward, an unknown commit or request, and a push for another task moves only that task's ref", async (t) => {
|
||||
const w = await setup(t);
|
||||
const { coder, reviewer, pm } = w.roles;
|
||||
gitIn(coder.clone, "checkout", "-q", "-b", "feat");
|
||||
const c1 = commit(coder.clone, "two");
|
||||
await w.push("coder");
|
||||
const refs = w.refs();
|
||||
|
||||
// The reviewer and pm hold no git.push.working; the broker refuses before the host takes a byte.
|
||||
for (const role of ["reviewer", "pm"]) {
|
||||
gitIn(w.roles[role].clone, "checkout", "-q", "-b", "feat");
|
||||
commit(w.roles[role].clone, `${role} work`);
|
||||
await rejects(w.push(role), "decision-required");
|
||||
}
|
||||
assert.equal(w.refs(), refs);
|
||||
|
||||
// A rewritten branch.
|
||||
gitIn(coder.clone, "update-ref", "-d", "refs/remotes/working/work/vikunja-1-2");
|
||||
gitIn(coder.clone, "reset", "-q", "--hard", w.main);
|
||||
commit(coder.clone, "other two");
|
||||
await rejects(w.push("coder"), "non-fast-forward");
|
||||
assert.equal(w.refs(), refs);
|
||||
|
||||
// The same branch for another task lands on that task's ref and leaves this one.
|
||||
const other = await w.push("coder", { task_ref: "vikunja:1/3" });
|
||||
assert.equal(other.ref, "work/vikunja-1-3");
|
||||
assert.match(w.refs(), new RegExp(`refs/heads/work/vikunja-1-2 ${c1}`));
|
||||
|
||||
await rejects(coder.client.call("review.request", { task_ref: TASK, commit: "e".repeat(40), to: "reviewer" }), "unknown-commit");
|
||||
const req = await coder.client.call("review.request", { task_ref: TASK, commit: c1, to: "reviewer" });
|
||||
await rejects(w.fetch("reviewer", "no-such-request"), "request-not-found");
|
||||
await rejects(w.fetch("reviewer", req.request, { task_ref: "vikunja:1/3" }), "request-not-found");
|
||||
// The coder and pm hold no review.verdict.
|
||||
await rejects(w.fetch("coder", req.request), "not-authorized");
|
||||
await rejects(w.fetch("pm", req.request), "not-authorized");
|
||||
assert.equal(existsSync(join(reviewer.clone, ".git", "refs", "remotes", "working")), false);
|
||||
|
||||
// Each refusal is an action.refused event on the task, from the role that tried.
|
||||
const refused = (await w.trail("action.refused")).map((e) => `${e.actor_role} ${e.body.code}`);
|
||||
for (const r of ["reviewer decision-required", "pm decision-required", "coder non-fast-forward", "coder unknown-commit", "reviewer request-not-found", "coder not-authorized", "pm not-authorized"])
|
||||
assert.ok(refused.includes(r), `${r} in ${refused.join(", ")}`);
|
||||
});
|
||||
|
||||
test("the size cap refuses a push before any byte", async (t) => {
|
||||
const w = await setup(t, { bundleMax: 64 });
|
||||
gitIn(w.roles.coder.clone, "checkout", "-q", "-b", "feat");
|
||||
commit(w.roles.coder.clone, "two");
|
||||
await rejects(w.push("coder"), "bundle-too-large");
|
||||
assert.equal(w.refs(), "");
|
||||
assert.deepEqual((await w.trail("action.allowed")).filter((e) => e.body.action === "git.push.working"), []);
|
||||
});
|
||||
|
||||
test("a fetch over the cap refuses before any byte reaches the reviewer", async (t) => {
|
||||
const w = await setup(t, { bundleMax: 1024 * 1024 });
|
||||
const { coder } = w.roles;
|
||||
gitIn(coder.clone, "checkout", "-q", "-b", "feat");
|
||||
// Two pushes of 600 KiB of noise each pass a 1 MiB cap; the reviewer has
|
||||
// neither, so its bundle carries both and is over.
|
||||
writeFileSync(join(coder.clone, "big"), randomBytes(600 * 1024));
|
||||
gitIn(coder.clone, "add", "big");
|
||||
gitIn(coder.clone, "commit", "-q", "-m", "big");
|
||||
const c = (await w.push("coder")).commit;
|
||||
writeFileSync(join(coder.clone, "big"), randomBytes(600 * 1024));
|
||||
gitIn(coder.clone, "commit", "-q", "-am", "bigger");
|
||||
const c2 = (await w.push("coder")).commit;
|
||||
assert.notEqual(c, c2);
|
||||
const req = await coder.client.call("review.request", { task_ref: TASK, commit: c2, to: "reviewer" });
|
||||
await rejects(w.fetch("reviewer", req.request), "bundle-too-large");
|
||||
assert.equal(existsSync(join(w.roles.reviewer.clone, ".git", "refs", "remotes", "working")), false);
|
||||
});
|
||||
|
||||
test("the socket refuses a request with no capability, an unknown one, and a bad request line", async (t) => {
|
||||
const w = await setup(t);
|
||||
assert.deepEqual(await raw(w.sock.path, "not json\n"), { ok: false, error: "invalid-request" });
|
||||
assert.deepEqual(await raw(w.sock.path, `${JSON.stringify({ op: "push" })}\n`), { ok: false, error: "unauthenticated" });
|
||||
assert.deepEqual(await raw(w.sock.path, `${JSON.stringify({ cap: "f".repeat(64), op: "push" })}\n`), { ok: false, error: "unauthenticated" });
|
||||
const cap = w.roles.coder.cap;
|
||||
assert.deepEqual(await raw(w.sock.path, `${JSON.stringify({ cap, op: "merge" })}\n`), { ok: false, error: "invalid-request" });
|
||||
assert.deepEqual(await raw(w.sock.path, `${JSON.stringify({ cap, op: "push", task_ref: TASK, project: "nope", branch: "feat", size: 10 })}\n`), { ok: false, error: "invalid-request" });
|
||||
assert.deepEqual(await raw(w.sock.path, `${JSON.stringify({ cap, op: "push", task_ref: "gitea:1", project: "stack", branch: "feat", size: 10 })}\n`), { ok: false, error: "invalid-request" });
|
||||
// Bytes before "send" are refused, not read.
|
||||
assert.deepEqual(await raw(w.sock.path, `${JSON.stringify({ cap, op: "push", task_ref: TASK, project: "stack", branch: "feat", size: 10 })}\n0123456789`), { ok: false, error: "invalid-request" });
|
||||
assert.deepEqual(await raw(w.sock.path, `${JSON.stringify({ cap, op: "fetch", task_ref: TASK, request: "x", have: ["nope"] })}\n`), { ok: false, error: "invalid-request" });
|
||||
// A push whose bytes have a bundle's header and then no ref line git can read.
|
||||
const junk = await new Promise((resolve, reject) => {
|
||||
const s = connect(w.sock.path);
|
||||
let buf = "";
|
||||
s.on("connect", () => s.write(`${JSON.stringify({ cap, op: "push", task_ref: TASK, project: "stack", branch: "feat", size: 20 })}\n`));
|
||||
s.on("data", (b) => {
|
||||
buf += b;
|
||||
if (buf === `${JSON.stringify({ ok: true, send: 20 })}\n`) s.write("# v2 git bundle\nzzz\n");
|
||||
});
|
||||
s.on("end", () => resolve(buf.trim().split("\n").pop()));
|
||||
s.on("error", reject);
|
||||
});
|
||||
assert.deepEqual(JSON.parse(junk), { ok: false, error: "invalid-bundle" });
|
||||
assert.equal(w.refs(), `refs/heads/main ${w.main}`);
|
||||
});
|
||||
|
||||
test("a rigged clone runs nothing: hooks, fsmonitor, a filter, a gitlink and a symlinked .git/config leave no marker", async (t) => {
|
||||
const w = await setup(t);
|
||||
const { coder, reviewer } = w.roles;
|
||||
const marks = join(w.root, "marks");
|
||||
mkdirSync(marks);
|
||||
gitIn(coder.clone, "checkout", "-q", "-b", "feat");
|
||||
commit(coder.clone, "two");
|
||||
// A gitlink whose .gitmodules would run a command if anything cloned it.
|
||||
// (An `update = !cmd` line fsck refuses outright; the end of the test pushes one.)
|
||||
writeFileSync(join(coder.clone, ".gitmodules"), `[submodule "sub"]\n\tpath = sub\n\turl = ext::sh -c touch% ${marks}/submodule\n`);
|
||||
gitIn(coder.clone, "add", ".gitmodules");
|
||||
gitIn(coder.clone, "update-index", "--add", "--cacheinfo", `160000,${w.main},sub`);
|
||||
gitIn(coder.clone, "commit", "-q", "-m", "gitlink");
|
||||
writeFileSync(join(coder.clone, ".gitattributes"), "* filter=evil\n");
|
||||
gitIn(coder.clone, "add", ".gitattributes");
|
||||
gitIn(coder.clone, "commit", "-q", "-m", "attributes");
|
||||
const tipCommit = gitIn(coder.clone, "rev-parse", "HEAD");
|
||||
// On a second branch, a .gitmodules with a command as its update setting.
|
||||
gitIn(coder.clone, "checkout", "-q", "-b", "bad");
|
||||
writeFileSync(join(coder.clone, ".gitmodules"), `[submodule "sub"]\n\tpath = sub\n\turl = ./sub\n\tupdate = !touch ${marks}/update\n`);
|
||||
gitIn(coder.clone, "add", ".gitmodules");
|
||||
gitIn(coder.clone, "commit", "-q", "-m", "update command");
|
||||
gitIn(coder.clone, "checkout", "-q", "feat");
|
||||
// Rigged only now, so the test's own git calls above ran none of it.
|
||||
const rig = (clone, name) => {
|
||||
const hooks = join(w.root, `hooks-${name}`);
|
||||
mkdirSync(hooks);
|
||||
for (const hook of ["reference-transaction", "pre-push", "post-checkout", "post-merge", "post-rewrite", "pre-auto-gc", "fsmonitor-watchman"]) {
|
||||
writeFileSync(join(hooks, hook), `#!/bin/sh\ntouch '${marks}/${name}-${hook}'\nexit 0\n`);
|
||||
chmodSync(join(hooks, hook), 0o755);
|
||||
mkdirSync(join(clone, ".git", "hooks"), { recursive: true });
|
||||
writeFileSync(join(clone, ".git", "hooks", hook), `#!/bin/sh\ntouch '${marks}/${name}-local-${hook}'\nexit 0\n`);
|
||||
chmodSync(join(clone, ".git", "hooks", hook), 0o755);
|
||||
}
|
||||
const config = join(w.root, `config-${name}`);
|
||||
writeFileSync(
|
||||
config,
|
||||
`${readFileSync(join(clone, ".git", "config"), "utf8")}[core]\n\thooksPath = ${hooks}\n\tfsmonitor = ${hooks}/fsmonitor-watchman\n[filter "evil"]\n\tclean = touch ${marks}/${name}-clean\n\tsmudge = touch ${marks}/${name}-smudge\n\tprocess = touch ${marks}/${name}-process\n[uploadpack]\n\tpackObjectsHook = touch ${marks}/${name}-pack\n[gc]\n\tauto = 1\n`,
|
||||
);
|
||||
renameSync(join(clone, ".git", "config"), join(w.root, `config-${name}.orig`));
|
||||
symlinkSync(config, join(clone, ".git", "config"));
|
||||
};
|
||||
rig(coder.clone, "coder");
|
||||
rig(reviewer.clone, "reviewer");
|
||||
const pushed = await w.push("coder");
|
||||
assert.equal(pushed.commit, tipCommit);
|
||||
const req = await coder.client.call("review.request", { task_ref: TASK, commit: pushed.commit, to: "reviewer" });
|
||||
assert.equal((await w.fetch("reviewer", req.request)).commit, pushed.commit);
|
||||
assert.deepEqual(readdirSync(marks), []);
|
||||
// The gitlink came across as a tree entry and nothing more.
|
||||
assert.match(gitIn(reviewer.clone, "-c", "core.hooksPath=/dev/null", "ls-tree", pushed.commit, "sub"), /^160000 commit /);
|
||||
assert.equal(existsSync(join(reviewer.clone, "sub", ".git")), false);
|
||||
assert.deepEqual(readdirSync(marks), []);
|
||||
// The update command: the host's fsck refuses the bundle and the work ref stays.
|
||||
const landed = w.refs();
|
||||
await rejects(w.push("coder", { branch: "bad" }), "invalid-bundle");
|
||||
assert.equal(w.refs(), landed);
|
||||
assert.deepEqual(readdirSync(marks), []);
|
||||
});
|
||||
@@ -13,7 +13,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "../../bus/src/client.mjs";
|
||||
import { launchLogFile, launchSocketPath, readSessions, Registry, SESSION_CREDENTIALS, sessionsFile, workspaceDir } from "../../seat/src/session.mjs";
|
||||
import { gitSocketPath, launchLogFile, launchSocketPath, readSessions, Registry, SESSION_CREDENTIALS, sessionsFile, workspaceDir } from "../../seat/src/session.mjs";
|
||||
import { sandboxProblem } from "../../seat/src/sandbox.mjs";
|
||||
import { gitEnv } from "../../seat/src/checkout.mjs";
|
||||
import { bootConfig, loadSystem } from "../src/config.mjs";
|
||||
@@ -150,6 +150,9 @@ test("bus start --pm: the PM runs under its own PID namespace, registered, with
|
||||
assert.equal(session.launchedBy, "jason");
|
||||
assert.equal(session.brokerSocket, host.path);
|
||||
assert.equal(session.launchSocket, sock);
|
||||
// Row 93: the PM neither pushes nor reviews, so no git socket; the launcher serves one.
|
||||
assert.equal(session.gitSocket, null);
|
||||
assert.equal(statSync(gitSocketPath(f.dataRoot)).mode & 0o777, 0o600);
|
||||
const manifest = JSON.parse(readFileSync(join(runDir, "bundle", "manifest.json"), "utf8"));
|
||||
assert.equal(manifest.piVersion, "0.85.1");
|
||||
assert.equal(manifest.claudeVersion, null);
|
||||
@@ -218,6 +221,11 @@ test("the PM launches a coder through its launch tool; the coder answers; refusa
|
||||
const bound = (pid) => readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").includes(sock);
|
||||
assert.equal(bound(readSessions(f.dataRoot).find((s) => s.instance === "pm").pid), true);
|
||||
assert.equal(bound(coder.pid), false);
|
||||
// Row 93: and only a role that pushes or reviews has the git socket.
|
||||
const gitBound = (pid) => readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").includes(gitSocketPath(f.dataRoot));
|
||||
assert.equal(gitBound(coder.pid), true);
|
||||
assert.equal(gitBound(readSessions(f.dataRoot).find((s) => s.instance === "pm").pid), false);
|
||||
assert.equal(JSON.parse(readFileSync(join(coder.runDir, "session.json"), "utf8")).gitSocket, gitSocketPath(f.dataRoot));
|
||||
|
||||
// The coder claimed its role and answers its own messages.
|
||||
const hello = await client.call("message.send", { to: "coder", class: "REQUEST", body: "hello" });
|
||||
|
||||
Reference in New Issue
Block a user