zsh: hold the mesh's login-shell seat, source the environment, and leave the rest to slots
The seat is now the mesh's node-login-shell, which a shell module claims rather than declares (novox/hq ADR 0204), and the environment is one module's that every module contributes to (ADR 0203). Per hq to-be 41 WP3: - no seat declaration; the claim is node-login-shell serving execute; - EDITOR, VISUAL, XDG_CONFIG_HOME and the three PATH entries are an environment contribution, not exports in the block; - a ~/.zshenv block sources ~/.config/mesh/environment.sh, so a script, a login and execute all see the environment; - the ~/.zshrc block goes at the start, so the operator's lines run after it, and holds today's shared defaults between the first, normal and last slots. The prompt, the plugins and the operator's own lines are no longer in it; - execute is bounded below the runtime's call limit (20 s default, 25 s at most), kills its whole process group on timeout, cuts each stream at 256 KiB and says so, runs in the account's home without the mesh's words, with the account's session words. The dead runuser branch is gone, because the runtime is the account; - zsh_config shows both files with their block line counts; - the README lists the one-off migration (ADR 0182).
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
// node-login-shell's execute over real child processes (novox/hq ADR 0204 §4, to-be 41 WP3): bounded
|
||||
// in time with its whole process group ended, bounded in output and saying so, run in the account's
|
||||
// home, answering the exit status. `sh` stands in for zsh where the test needs no zsh of its own, so
|
||||
// the bounds are proven wherever the tests run; one test runs zsh itself when it is installed.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, realpathSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS, commandEnv, execute, homeOf, timeoutOf, zshFile } from "../shell.ts";
|
||||
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), "zsh-execute-")));
|
||||
const base = { shell: "sh", cwd: dir, env: { PATH: process.env.PATH, HOME: dir }, timeoutSeconds: 5 };
|
||||
|
||||
function alive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function gone(pid: number, ms = 2000): Promise<boolean> {
|
||||
const until = Date.now() + ms;
|
||||
while (Date.now() < until) {
|
||||
if (!alive(pid)) return true;
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
return !alive(pid);
|
||||
}
|
||||
|
||||
test("the exit status and both streams are answered", async () => {
|
||||
const r = await execute("echo out; echo err >&2; exit 3", base);
|
||||
assert.equal(r.status, 3);
|
||||
assert.equal(r.stdout, "out\n");
|
||||
assert.equal(r.stderr, "err\n");
|
||||
assert.equal(r.timed_out, false);
|
||||
assert.deepEqual(r.truncated, { stdout: false, stderr: false });
|
||||
});
|
||||
|
||||
test("it runs in the directory it is given", async () => {
|
||||
const r = await execute("pwd", base);
|
||||
assert.equal(r.stdout.trim(), dir);
|
||||
assert.equal(r.cwd, dir);
|
||||
});
|
||||
|
||||
test("on timeout the whole process group is ended, a backgrounded grandchild included", async () => {
|
||||
const pidfile = join(dir, "grandchild.pid");
|
||||
const started = Date.now();
|
||||
const r = await execute(`sh -c 'sleep 100 & echo $! > ${pidfile}; wait'`, { ...base, timeoutSeconds: 0.5 });
|
||||
assert.equal(r.timed_out, true);
|
||||
assert.ok(Date.now() - started < 5000, "answered soon after the timeout, not when the sleep ended");
|
||||
assert.equal(r.status, null);
|
||||
assert.equal(r.signal, "SIGKILL");
|
||||
const pid = Number(readFileSync(pidfile, "utf8").trim());
|
||||
assert.ok(pid > 0);
|
||||
assert.ok(await gone(pid), `the grandchild ${pid} is gone`);
|
||||
});
|
||||
|
||||
test("output beyond the bound is cut and the answer says so", async () => {
|
||||
const r = await execute("head -c 300000 /dev/zero | tr '\\0' a; head -c 10 /dev/zero | tr '\\0' b >&2", { ...base, capBytes: 1024 });
|
||||
assert.equal(r.stdout.length, 1024);
|
||||
assert.equal(r.truncated.stdout, true);
|
||||
assert.equal(r.stderr, "bbbbbbbbbb");
|
||||
assert.equal(r.truncated.stderr, false);
|
||||
assert.equal(r.status, 0);
|
||||
});
|
||||
|
||||
test("a job left in the background does not hold the answer", async () => {
|
||||
const pidfile = join(dir, "background.pid");
|
||||
const started = Date.now();
|
||||
const r = await execute(`sleep 30 & echo $! > ${pidfile}; echo done`, base);
|
||||
assert.ok(Date.now() - started < 3000);
|
||||
assert.equal(r.stdout, "done\n");
|
||||
assert.equal(r.status, 0);
|
||||
process.kill(Number(readFileSync(pidfile, "utf8").trim()), "SIGKILL");
|
||||
});
|
||||
|
||||
test("zsh runs the command as a login shell, when zsh is here", async (t) => {
|
||||
try {
|
||||
execFileSync("zsh", ["-c", "true"]);
|
||||
} catch {
|
||||
t.skip("zsh is not installed here");
|
||||
return;
|
||||
}
|
||||
const home = join(dir, "home");
|
||||
mkdirSync(home, { recursive: true });
|
||||
writeFileSync(join(home, ".zshenv"), "export FROM_ZSHENV=yes\n");
|
||||
const r = await execute("print -r -- $FROM_ZSHENV; [[ -o login ]] && print login", { cwd: home, env: { PATH: process.env.PATH, HOME: home, ZDOTDIR: home }, timeoutSeconds: 5 });
|
||||
assert.equal(r.stdout, "yes\nlogin\n");
|
||||
assert.equal(r.status, 0);
|
||||
});
|
||||
|
||||
test("the timeout is held below the runtime's call limit", () => {
|
||||
assert.equal(timeoutOf(undefined), DEFAULT_TIMEOUT_SECONDS);
|
||||
assert.equal(timeoutOf(5), 5);
|
||||
assert.equal(timeoutOf(600), MAX_TIMEOUT_SECONDS);
|
||||
assert.equal(timeoutOf(-1), DEFAULT_TIMEOUT_SECONDS);
|
||||
assert.equal(timeoutOf("x"), DEFAULT_TIMEOUT_SECONDS);
|
||||
assert.ok(MAX_TIMEOUT_SECONDS < 30 && DEFAULT_TIMEOUT_SECONDS <= MAX_TIMEOUT_SECONDS);
|
||||
});
|
||||
|
||||
test("the command's environment drops the mesh's words and gains the account's session words when they exist", () => {
|
||||
const env = { PATH: "/usr/bin", HOME: "/home/op", MESH_OPERATOR_ACCOUNT: "op", MESH_OPERATOR_HOME: "/home/op", MESH_ANYTHING: "x" };
|
||||
const there = commandEnv(env, 1000, (p) => p === "/run/user/1000");
|
||||
assert.deepEqual(there, { PATH: "/usr/bin", HOME: "/home/op", XDG_RUNTIME_DIR: "/run/user/1000", DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/1000/bus" });
|
||||
const absent = commandEnv(env, 1000, () => false);
|
||||
assert.deepEqual(absent, { PATH: "/usr/bin", HOME: "/home/op" });
|
||||
assert.equal(homeOf(env), "/home/op");
|
||||
assert.equal(homeOf({ HOME: "/h" }), "/h");
|
||||
});
|
||||
|
||||
test("zsh_config counts the mesh's block in a file", async () => {
|
||||
const path = join(dir, ".zshrc");
|
||||
writeFileSync(path, "# BEGIN mesh zsh.rc\na\nb\nc\n# END mesh zsh.rc\nmine\n");
|
||||
const r = await zshFile(path);
|
||||
assert.equal(r.lines, 6);
|
||||
assert.equal(r.mesh_block_lines, 3);
|
||||
assert.deepEqual(await zshFile(join(dir, "absent")), { path: join(dir, "absent"), exists: false });
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
// The zsh module's shape (novox/hq ADR 0203, ADR 0204, to-be 41 WP3): it claims the mesh's
|
||||
// node-login-shell and declares no seat; its ~/.zshrc block goes at the start and holds the three
|
||||
// slots in order; its ~/.zshenv block sources the path node-environment fixes and nothing else; its
|
||||
// environment is a contribution, never an export in its own lines.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const m = JSON.parse(readFileSync(new URL("../module.json", import.meta.url), "utf8"));
|
||||
const resource = (id: string) => m.resources.find((r: { id: string }) => r.id === id);
|
||||
|
||||
test("it claims node-login-shell serving execute, and declares no seat", () => {
|
||||
assert.equal(m.seats, undefined);
|
||||
assert.deepEqual(m.claims, [{ name: "node-login-shell", scope: "node", serves: ["execute"] }]);
|
||||
});
|
||||
|
||||
test("the .zshrc block is at the start and holds the three slots in order, after the defaults' header", () => {
|
||||
const rc = resource("rc");
|
||||
assert.equal(rc.path, "${machine:account-home}/.zshrc");
|
||||
assert.equal(rc.into, "block");
|
||||
assert.equal(rc.at, "start");
|
||||
const c: string = rc.content;
|
||||
const first = c.indexOf("${shell:zsh:first}");
|
||||
const normal = c.indexOf("${shell:zsh:normal}");
|
||||
const last = c.indexOf("${shell:zsh:last}");
|
||||
assert.ok(c.startsWith("# The mesh's block"), "it opens by saying whose it is");
|
||||
assert.ok(first > 0 && first < normal && normal < last, "first, normal, last, in that order");
|
||||
assert.ok(c.indexOf("set_terminal_title") > first && c.indexOf("sudo-disksize") < normal, "the defaults sit between first and normal");
|
||||
for (const slot of ["first", "normal", "last"]) assert.equal(c.split(`\${shell:zsh:${slot}}`).length, 2, `${slot} appears once`);
|
||||
});
|
||||
|
||||
test("the .zshrc block carries the shared defaults and none of the operator's own lines", () => {
|
||||
const c: string = resource("rc").content;
|
||||
for (const want of ["precmd_functions+=(set_terminal_title)", "alias egrep='egrep --color=auto'", "alias ll='ls -alhF'", "alias drun=", "disksize()", "sudo-disksize()"]) {
|
||||
assert.ok(c.includes(want), want);
|
||||
}
|
||||
assert.equal((c.match(/^bindkey /gm) ?? []).length, 7);
|
||||
for (const not of ["export ", "p10k", "powerlevel10k", "nvm", ".dotnet", "MY_KV_PATH", "MY_LIB_PATH", "CLAUDE_CODE_DISABLE_TERMINAL_TITLE", "killport", "findport", ".zshrc.local", "zsh-autosuggestions", "zsh-syntax-highlighting"]) {
|
||||
assert.ok(!c.includes(not), `the block holds no ${not}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("the .zshenv block sources exactly the environment file node-environment fixes", () => {
|
||||
const env = resource("env");
|
||||
assert.equal(env.path, "${machine:account-home}/.zshenv");
|
||||
assert.equal(env.into, "block");
|
||||
assert.equal(env.at, "start");
|
||||
const sourced = [...(env.content as string).matchAll(/^\s*(?:source|\.)\s+"?([^"\s]+)"?/gm)].map((x) => x[1]);
|
||||
assert.deepEqual(sourced, ["${machine:account-home}/.config/mesh/environment.sh"]);
|
||||
});
|
||||
|
||||
test("its environment is a contribution: the editor, the configuration home, and three PATH entries at the start", () => {
|
||||
assert.deepEqual(m.environment.variables, { EDITOR: "vim", VISUAL: "vim", XDG_CONFIG_HOME: "${machine:account-home}/.config" });
|
||||
assert.deepEqual(m.environment.path, [
|
||||
{ entry: "${machine:account-home}/.local/bin", at: "start" },
|
||||
{ entry: "${machine:account-home}/scripts", at: "start" },
|
||||
{ entry: "${machine:account-home}/scripts/bin", at: "start" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("it keeps the package and sets the login shell through the user shape", () => {
|
||||
assert.deepEqual(resource("package"), { id: "package", type: "package", package: "zsh" });
|
||||
assert.deepEqual(resource("login"), { id: "login", type: "user", name: "${machine:account}", shell: "/usr/bin/zsh" });
|
||||
assert.ok(m.capabilities.includes("package-manager"));
|
||||
});
|
||||
|
||||
test("the tools bundle registers the seat's verb under node-login-shell", () => {
|
||||
const src = readFileSync(new URL("../tools/index.ts", import.meta.url), "utf8");
|
||||
assert.match(src, /registerModuleTools\("node-login-shell", seatVerbs\)/);
|
||||
assert.doesNotMatch(src, /runuser|"login-shell"/);
|
||||
});
|
||||
Reference in New Issue
Block a user