seat: give role sessions a git identity and report the checkout commit (row 104, #1598)
sandboxArgv sets GIT_AUTHOR_NAME/EMAIL and GIT_COMMITTER_NAME/EMAIL from
the instance and business ids (name = instance id, email
<instance>@<business>.mosaic.invalid); spawnSession and the launcher pass
the ids through. checkout results carry the clone's commit (a kept clone
gets none), and the bundle prompt names it. allowed() is unchanged.
Packet agents/darkwing/work/queue-104/r1/ (manifest f99f2ae7). Dewey
approved round 1 in 28293 (posted under jarvis by mistake; 28294 is
Dewey's confirmation). Sage's gate on f00613106 plus the candidate:
every package suite and scripts/test-*.sh green (test-release failed 3
activation checks once, not reproduced; rerun 14/0).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -83,7 +83,7 @@ async function once(w) {
|
||||
|
||||
test("a first launch clones the root at its branch, with the root's path as origin", async (t) => {
|
||||
const w = world(t);
|
||||
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "cloned", branch: "main" });
|
||||
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "cloned", branch: "main", commit: git(w.root, "rev-parse", "HEAD") });
|
||||
assert.equal(git(w.clone, "symbolic-ref", "--short", "HEAD"), "main");
|
||||
assert.equal(git(w.clone, "rev-parse", "HEAD"), git(w.root, "rev-parse", "HEAD"));
|
||||
assert.equal(git(w.clone, "remote", "get-url", "origin"), w.root);
|
||||
@@ -116,10 +116,10 @@ test("a later launch fast-forwards a clean clone after a commit to the root, the
|
||||
const w = world(t);
|
||||
await once(w);
|
||||
const head = commit(w.root, "two\n");
|
||||
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "updated", branch: "main" });
|
||||
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "updated", branch: "main", commit: head });
|
||||
assert.equal(git(w.clone, "rev-parse", "HEAD"), head);
|
||||
assert.equal(readFileSync(join(w.clone, "README"), "utf8"), "two\n");
|
||||
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "current", branch: "main" });
|
||||
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "current", branch: "main", commit: head });
|
||||
assert.equal(git(w.clone, "status", "--porcelain"), "");
|
||||
});
|
||||
|
||||
@@ -127,7 +127,7 @@ test("a clone ahead of the root keeps its commits and counts as current", async
|
||||
const w = world(t);
|
||||
await once(w);
|
||||
const mine = commit(w.clone, "mine\n");
|
||||
assert.equal((await once(w)).outcome, "current");
|
||||
assert.deepEqual(await once(w), { project: "stack", path: w.clone, outcome: "current", branch: "main", commit: mine });
|
||||
assert.equal(git(w.clone, "rev-parse", "HEAD"), mine);
|
||||
});
|
||||
|
||||
@@ -156,6 +156,57 @@ test("a dirty clone, one on another branch and one diverged are kept as they are
|
||||
}
|
||||
});
|
||||
|
||||
// Row 104 (#1598): a session that sets its own git identity in the clone's
|
||||
// config gets the clone kept at every later launch. The host only reads the
|
||||
// clone's config file and runs no git in it, so the kept result has no commit.
|
||||
test("a clone with user.name or user.email in its config is kept, and git runs nothing in it", async (t) => {
|
||||
for (const key of ["user.name", "user.email"]) {
|
||||
await t.test(key, async (t) => {
|
||||
const w = world(t);
|
||||
await once(w);
|
||||
git(w.clone, "config", key, "someone");
|
||||
commit(w.root, "two\n");
|
||||
const bin = join(w.dir, "bin");
|
||||
mkdirSync(bin);
|
||||
const real = spawnSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).stdout.trim();
|
||||
const calls = join(w.dir, "calls");
|
||||
writeFileSync(join(bin, "git"), `#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\nexec '${real}' "$@"\n`, { mode: 0o755 });
|
||||
const list = await ensureCheckouts({ workspace: w.workspace, projects: { stack: { root: w.root } }, env: { PATH: `${bin}:${process.env.PATH}` } });
|
||||
assert.deepEqual(list, [{ project: "stack", path: w.clone, outcome: "kept", reason: "config-changed" }]);
|
||||
// The one call naming the clone reads its config file, as unsafe() does for every clone.
|
||||
const named = readFileSync(calls, "utf8").split("\n").filter((l) => l.includes(w.clone));
|
||||
assert.deepEqual(named.map((l) => l.slice(l.indexOf(" config "))), [` config --file ${join(w.clone, ".git", "config")} --list --null`], "no git ran in the kept clone");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// When git can't read the clone's HEAD after the fast-forward, the commit is
|
||||
// null, not an empty string. A git on PATH fails the second rev-parse HEAD.
|
||||
test("a commit git can't read after the fast-forward is null", async (t) => {
|
||||
const w = world(t);
|
||||
await once(w);
|
||||
commit(w.root, "two\n");
|
||||
const bin = join(w.dir, "bin");
|
||||
mkdirSync(bin);
|
||||
const real = spawnSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).stdout.trim();
|
||||
const count = join(w.dir, "count");
|
||||
writeFileSync(join(bin, "git"), `#!/bin/sh\ncase " $* " in *" rev-parse HEAD ") echo x >> '${count}'; [ "$(wc -l < '${count}')" -ge 2 ] && exit 1;; esac\nexec '${real}' "$@"\n`, { mode: 0o755 });
|
||||
const list = await ensureCheckouts({ workspace: w.workspace, projects: { stack: { root: w.root } }, env: { PATH: `${bin}:${process.env.PATH}` } });
|
||||
assert.deepEqual(list, [{ project: "stack", path: w.clone, outcome: "updated", branch: "main", commit: null }]);
|
||||
});
|
||||
|
||||
// A new clone whose HEAD git can't read refuses like a failed clone, and its
|
||||
// temp directory goes. A git on PATH fails rev-parse --verify HEAD.
|
||||
test("a new clone whose commit can't be read refuses the launch and leaves nothing behind", async (t) => {
|
||||
const w = world(t);
|
||||
const bin = join(w.dir, "bin");
|
||||
mkdirSync(bin);
|
||||
const real = spawnSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).stdout.trim();
|
||||
writeFileSync(join(bin, "git"), `#!/bin/sh\ncase " $* " in *" rev-parse --verify HEAD ") echo 'fatal: no HEAD' >&2; exit 128;; esac\nexec '${real}' "$@"\n`, { mode: 0o755 });
|
||||
await assert.rejects(ensureCheckouts({ workspace: w.workspace, projects: { stack: { root: w.root } }, env: { PATH: `${bin}:${process.env.PATH}` } }), (e) => e instanceof CheckoutError && e.project === "stack" && /^the new clone of .* has no readable HEAD: /.test(e.message));
|
||||
assert.deepEqual(readdirSync(w.workspace), [], "no clone and no temp directory");
|
||||
});
|
||||
|
||||
// A repository outside the workspace whose config and hooks run a command
|
||||
// that touches `marker`, for the cases that point git there.
|
||||
function bait(dir, marker) {
|
||||
@@ -328,7 +379,7 @@ test("every project in the business file gets its own clone at its own branch",
|
||||
const workspace = join(dir, "ws");
|
||||
mkdirSync(workspace);
|
||||
const list = await ensureCheckouts({ workspace, projects: { alpha: { root: a }, beta: { root: b } } });
|
||||
assert.deepEqual(list.map((c) => [c.project, c.path, c.outcome, c.branch]), [["alpha", join(workspace, "alpha"), "cloned", "main"], ["beta", join(workspace, "beta"), "cloned", "dev"]]);
|
||||
assert.deepEqual(list.map((c) => [c.project, c.path, c.outcome, c.branch, c.commit]), [["alpha", join(workspace, "alpha"), "cloned", "main", git(a, "rev-parse", "HEAD")], ["beta", join(workspace, "beta"), "cloned", "dev", git(b, "rev-parse", "HEAD")]]);
|
||||
assert.equal(git(join(workspace, "beta"), "symbolic-ref", "--short", "HEAD"), "dev");
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Store } from "../../bus/src/store.mjs";
|
||||
import { SOURCES } from "../../harness/src/bundle.mjs";
|
||||
import { startTimeOf } from "../src/proc.mjs";
|
||||
import { newRun, RUNNER, runnerOf, sessionEnv, spawnSession, writeSessionFile } from "../src/session.mjs";
|
||||
import { binds, prepareRun, REPO, RUNNER_FILES, sandboxArgv, sandboxProblem } from "../src/sandbox.mjs";
|
||||
import { binds, gitIdentity, prepareRun, REPO, RUNNER_FILES, sandboxArgv, sandboxProblem } from "../src/sandbox.mjs";
|
||||
|
||||
const FAKE = new URL("../../harness/tests/fixtures/fake-adapter.mjs", import.meta.url).pathname;
|
||||
const skip = sandboxProblem() ?? false;
|
||||
@@ -80,6 +80,26 @@ test("the host's git socket is bound only when given, read-only and before the w
|
||||
assert.ok(RUNNER_FILES.includes(join(REPO, "packages/harness/src/git-tools.mjs")));
|
||||
});
|
||||
|
||||
// Row 104 (#1598): the git identity comes from the business and the role
|
||||
// instance, both checked as ids, and sandboxArgv sets it for every session.
|
||||
test("the git identity is the instance's, at <instance>@<business>.mosaic.invalid, and sandboxArgv refuses without it", (t) => {
|
||||
const email = "[email protected]";
|
||||
assert.deepEqual(gitIdentity({ business: "acme", role: "coder" }), { GIT_AUTHOR_NAME: "coder", GIT_AUTHOR_EMAIL: email, GIT_COMMITTER_NAME: "coder", GIT_COMMITTER_EMAIL: email });
|
||||
for (const [business, role] of [[undefined, "coder"], ["acme", undefined], ["acme", ""], ["Acme", "coder"], ["acme", "coder\nx"], ["acme", "coder>"], ["acme", 7]]) {
|
||||
assert.throws(() => gitIdentity({ business, role }), /^Error: git identity: (business|role) .* isn't an id$/, `${business}/${role}`);
|
||||
}
|
||||
const root = scratch(t);
|
||||
const runDir = join(root, "run");
|
||||
const list = binds({ runDir, workspace: join(root, "ws"), adapter: join(root, "a.sh"), harness: "pi", brokerSocket: join(root, "bus.sock") });
|
||||
const argv = sandboxArgv(list, runDir, ["/bin/true"], { business: "acme", role: "coder" });
|
||||
const set = {};
|
||||
argv.slice(0, argv.indexOf("--")).forEach((a, i, all) => {
|
||||
if (a === "--setenv") set[all[i + 1]] = all[i + 2];
|
||||
});
|
||||
assert.deepEqual(set, { HOME: join(runDir, "home"), TMPDIR: "/tmp", ...gitIdentity({ business: "acme", role: "coder" }) });
|
||||
assert.throws(() => sandboxArgv(list, runDir, ["/bin/true"], {}), /git identity: business undefined isn't an id/);
|
||||
});
|
||||
|
||||
// The founder's files and the other sessions' state, as stand-ins. The
|
||||
// /dev/shm file and the SysV segment are the host's, made for this test and
|
||||
// removed after it.
|
||||
@@ -136,6 +156,10 @@ function checks({ s, ws, runDir, run, dataRoot, store }) {
|
||||
["write and read /tmp", "echo tmp > /tmp/probe && cat /tmp/probe", true, "tmp"],
|
||||
["run node", `'${process.execPath}' -e 'console.log(6 * 7)'`, true, "42"],
|
||||
["run pi, the session's harness", `'${join(REPO, "node_modules", ".bin", "pi")}' --version >/dev/null`, true],
|
||||
// Row 104 (#1598): a commit in a clone with no user config carries the
|
||||
// instance's identity from the environment, and the clone's config
|
||||
// gains no user key.
|
||||
["commit in a fresh clone as the instance", `d=$(mktemp -d) && cd "$d" && git init -q o && git -C o commit --allow-empty -q -m base && git clone -q o c && cd c && echo x > f && git add f && git commit -q -m m && ! git config --file .git/config --get-regexp '^user\\.' && git log -1 --format='%an <%ae>|%cn <%ce>'`, true, "coder <[email protected]>|coder <[email protected]>"],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -205,7 +229,7 @@ async function session(t, { expose = () => [] } = {}) {
|
||||
});
|
||||
|
||||
const paths = { workspace: ws, adapter, harness: "pi", brokerSocket, readOnly: [FAKE, ...expose(s)] };
|
||||
const child = spawnSession({ runDir, env: sessionEnv({ HOME: process.env.HOME, PATH: process.env.PATH }, run), paths });
|
||||
const child = spawnSession({ runDir, env: sessionEnv({ HOME: process.env.HOME, PATH: process.env.PATH }, run), paths, business: "acme", role: "coder" });
|
||||
const exited = new Promise((r) => child.once("close", (code, signal) => r({ code, signal })));
|
||||
t.after(async () => {
|
||||
try {
|
||||
@@ -263,7 +287,7 @@ test("a bind that exposes a stand-in, or a missing workspace bind, fails the mat
|
||||
// without the workspace's.
|
||||
const all = binds({ runDir: x.runDir, workspace: x.ws, adapter: x.adapter, harness: "pi", brokerSocket: x.brokerSocket });
|
||||
const direct = (list) => async (cmd) => {
|
||||
const argv = sandboxArgv(list, x.runDir, ["/bin/sh", "-c", cmd]);
|
||||
const argv = sandboxArgv(list, x.runDir, ["/bin/sh", "-c", cmd], { business: "acme", role: "coder" });
|
||||
const r = spawnSync(argv[0], argv.slice(1), { encoding: "utf8", timeout: 10000 });
|
||||
return { rc: r.status, out: (r.stdout + r.stderr).replace(/\n$/, "") };
|
||||
};
|
||||
|
||||
@@ -192,7 +192,7 @@ test("a session runs in its sandbox as pid 1 of its namespace, claims, answers,
|
||||
});
|
||||
|
||||
const paths = { workspace: ws, adapter, harness: "pi", brokerSocket, readOnly: [FAKE] };
|
||||
const child = spawnSession({ runDir, env: sessionEnv({ HOME: process.env.HOME, PATH: process.env.PATH, GITEA_TOKEN: "x" }, run), paths });
|
||||
const child = spawnSession({ runDir, env: sessionEnv({ HOME: process.env.HOME, PATH: process.env.PATH, GITEA_TOKEN: "x" }, run), paths, business: "acme", role: "coder" });
|
||||
t.after(() => {
|
||||
try {
|
||||
process.kill(child.pid, "SIGKILL");
|
||||
|
||||
Reference in New Issue
Block a user