Files
SeedPass/js/packages/cli/test/sinks.test.ts
T
thePR0M3TH3ANandClaude Opus 5 73ac93a9f1 Deliver the oversize refusal before hanging up, and pin a POSIX-only test
Two left from the platform run, one real.

THE DAEMON HUNG UP WITHOUT SAYING WHY, ON macOS ONLY

The line-length cap did `socket.write(refusal)` and then `socket.destroy()`.
destroy() tears the socket down immediately and discards whatever is still
buffered — on Linux the write usually reaches the kernel first and the peer
sees "request too large", on macOS it does not and the peer sees an
unexplained close. The refusal itself worked on both; only the reason was
lost, and only on one platform, which is why nothing noticed until this suite
ran there.

Now `end(payload, cb)` and destroy from the callback, so the bytes are
flushed before teardown, plus a `refused` flag so a peer still pushing the
rest of its megabyte is ignored rather than re-buffered.

The test that caught it was itself half the problem: it treated its own EPIPE
as fatal, when a client's broken pipe is the expected consequence of a server
correctly hanging up mid-write. Fixed in the previous commit; this one fixes
what that then exposed.

AND ONE TEST ASSERTING POSIX BEHAVIOUR WITHOUT SAYING SO

`honours backslash escapes outside single quotes` called parseCommandSpec with
no platform flag, so it read process.platform and asserted POSIX semantics on
Windows — where backslash is a path separator and deliberately is not an
escape. It now passes `{ windows: false }`, because that is the branch it is
about.

Windows went 15 failures to 1 to 0; macOS 2 to 1 to 0.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-20 14:47:24 -04:00

264 lines
10 KiB
TypeScript

/**
* Sink command-spec parsing.
*
* `--exec 'sh -c "..."'` is the most common sink invocation there is, and
* splitting the spec on whitespace alone broke exactly that shape: sh
* received the fragments of the quoted script as separate argv entries and
* died on an unterminated string.
*/
import { describe, expect, it } from "vitest";
import { NO_CLIPBOARD } from "./helpers/platform.js";
import { parseCommandSpec } from "../src/sinks.js";
describe("parseCommandSpec", () => {
it("keeps a pre-split argv untouched", () => {
expect(parseCommandSpec(["wc", "-c"])).toEqual(["wc", ["-c"]]);
});
it("splits a single quoted spec on whitespace", () => {
expect(parseCommandSpec(["wc -c"])).toEqual(["wc", ["-c"]]);
});
it("treats a double-quoted section as one argument", () => {
expect(parseCommandSpec(['sh -c "echo hello world"'])).toEqual([
"sh",
["-c", "echo hello world"],
]);
});
it("treats a single-quoted section as one argument", () => {
expect(parseCommandSpec(["sh -c 'echo hello world'"])).toEqual([
"sh",
["-c", "echo hello world"],
]);
});
it("supports quotes nested inside the other quote style", () => {
expect(parseCommandSpec(["sh -c \"echo 'hi there'\""])).toEqual([
"sh",
["-c", "echo 'hi there'"],
]);
});
// Explicitly the POSIX branch. Without the flag this reads process.platform
// and asserts POSIX behaviour on Windows, where backslash is a path
// separator and deliberately is not an escape.
it("honours backslash escapes outside single quotes", () => {
expect(parseCommandSpec(["cmd a\\ b"], { windows: false })).toEqual(["cmd", ["a b"]]);
expect(parseCommandSpec(["cmd 'a\\ b'"], { windows: false })).toEqual([
"cmd",
["a\\ b"],
]);
});
it("collapses runs of whitespace", () => {
expect(parseCommandSpec([" wc -c "])).toEqual(["wc", ["-c"]]);
});
it("does not expand variables — no shell is involved", () => {
// The secret arrives via the environment; a spec that looks like a shell
// expansion must reach the child literally rather than being substituted
// by us.
expect(parseCommandSpec(["cmd $SEEDPASS_SECRET"])).toEqual([
"cmd",
["$SEEDPASS_SECRET"],
]);
});
it("rejects an unterminated quote instead of guessing", () => {
expect(() => parseCommandSpec(['sh -c "oops'])).toThrow(/unterminated double quote/);
expect(() => parseCommandSpec(["sh -c 'oops"])).toThrow(/unterminated single quote/);
});
it("rejects an empty command", () => {
expect(() => parseCommandSpec([""])).toThrow(/empty command/);
});
});
import { clipboardSink } from "../src/sinks.js";
import { mkdtemp, writeFile, readFile, chmod } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import process from "node:process";
/**
* A fake wl-copy/wl-paste pair backed by a state file, placed first on PATH.
* clipboardSink tries wl-copy first, so these win over any real tools and the
* test does not depend on a display or a system clipboard.
*/
async function fakeClipboard(): Promise<{ dir: string; stateFile: string; restore: () => void }> {
const dir = await mkdtemp(join(tmpdir(), "seedpass-clip-"));
const stateFile = join(dir, "clip.txt");
await writeFile(stateFile, "");
await writeFile(join(dir, "wl-copy"), `#!/bin/sh\ncat > "${stateFile}"\n`);
// -n: no trailing newline, matching the real wl-paste flag clipboardSink uses.
await writeFile(join(dir, "wl-paste"), `#!/bin/sh\ncat "${stateFile}"\n`);
await chmod(join(dir, "wl-copy"), 0o755);
await chmod(join(dir, "wl-paste"), 0o755);
const savedPath = process.env["PATH"];
process.env["PATH"] = `${dir}:${savedPath ?? ""}`;
return {
dir,
stateFile,
restore: () => {
if (savedPath === undefined) delete process.env["PATH"];
else process.env["PATH"] = savedPath;
},
};
}
describe("clipboardSink auto-clear", () => {
it.skipIf(NO_CLIPBOARD)("copies the secret and, by default, never wipes it", async () => {
const clip = await fakeClipboard();
try {
await clipboardSink("s3cr3t");
expect(await readFile(clip.stateFile, "utf8")).toBe("s3cr3t");
// No clear scheduled: it stays put.
await new Promise((r) => setTimeout(r, 250));
expect(await readFile(clip.stateFile, "utf8")).toBe("s3cr3t");
} finally {
clip.restore();
}
});
it.skipIf(NO_CLIPBOARD)("wipes the secret after the delay when it is still ours", async () => {
const clip = await fakeClipboard();
try {
const r = await clipboardSink("wipe-me", { clearAfterSeconds: 0.15 });
expect(r.detail).toContain("clears in");
expect(await readFile(clip.stateFile, "utf8")).toBe("wipe-me");
await new Promise((res) => setTimeout(res, 400));
expect(await readFile(clip.stateFile, "utf8")).toBe("");
} finally {
clip.restore();
}
});
it("leaves a value the user copied since the secret untouched", async () => {
const clip = await fakeClipboard();
try {
await clipboardSink("old-secret", { clearAfterSeconds: 0.15 });
// The user copies something else before the timer fires.
await writeFile(clip.stateFile, "user's own copy");
await new Promise((res) => setTimeout(res, 400));
expect(await readFile(clip.stateFile, "utf8")).toBe("user's own copy");
} finally {
clip.restore();
}
});
})
describe("command spec parsing keeps every argument", () => {
it("does not tokenize when more than one element was supplied", () => {
// Mutation testing found this unguarded: turning the `&&` into `||` makes
// a multi-element spec whose FIRST element contains whitespace get
// tokenized, silently discarding every element after it. Losing an
// argument changes which command runs — dropping a `--dry-run` or a
// target path is exactly the kind of silent change that matters for a
// sink handed a secret.
expect(parseCommandSpec(["echo hi", "there"])).toEqual(["echo hi", ["there"]]);
expect(parseCommandSpec(["wc", "-c"])).toEqual(["wc", ["-c"]]);
expect(parseCommandSpec(["a b", "c d", "e"])).toEqual(["a b", ["c d", "e"]]);
});
it("tokenizes only a lone element containing whitespace", () => {
expect(parseCommandSpec(["wc -c"])).toEqual(["wc", ["-c"]]);
// A lone element with no whitespace needs no splitting, and splitting it
// must not change it.
expect(parseCommandSpec(["ls"])).toEqual(["ls", []]);
});
it("refuses an empty command rather than running something unintended", () => {
expect(() => parseCommandSpec([])).toThrow(/empty command/);
expect(() => parseCommandSpec([""])).toThrow(/empty command/);
});
});
import { sinkEnv } from "../src/sinks.js";
describe("the environment a sink child receives", () => {
it("passes only the allowlist through, and never the caller's own env", async () => {
process.env["SEEDPASS_TEST_LEAK"] = "must-not-appear";
try {
const env = sinkEnv();
expect(env["SEEDPASS_TEST_LEAK"]).toBeUndefined();
// PATH is on the allowlist and the child genuinely needs it, so its
// presence is what proves the allowlist is being applied rather than
// the whole environment simply being empty.
expect(env["PATH"]).toBe(process.env["PATH"]);
} finally {
delete process.env["SEEDPASS_TEST_LEAK"];
}
});
it("drops a SEEDPASS_ variable even if the allowlist grows one", async () => {
// The backstop. It cannot fire against today's allowlist — which is why
// deleting it changed nothing anywhere — so the allowlist is injected
// here to stand in for the future edit that adds one by mistake.
process.env["SEEDPASS_SNEAKY"] = "must-not-appear";
try {
const env = sinkEnv({}, ["PATH", "SEEDPASS_SNEAKY"]);
expect(env["SEEDPASS_SNEAKY"]).toBeUndefined();
expect(env["PATH"]).toBeDefined();
} finally {
delete process.env["SEEDPASS_SNEAKY"];
}
});
it("still injects the exec sink's own secret variable", async () => {
// `extra` is deliberately exempt from the SEEDPASS_ rule: handing the
// secret to the child through the environment is the exec sink's entire
// job. A backstop that swallowed it would break the feature.
const env = sinkEnv({ SEEDPASS_SECRET: "the-secret" });
expect(env["SEEDPASS_SECRET"]).toBe("the-secret");
});
});
describe("command specs on Windows", () => {
/**
* `windows` is an explicit parameter so both branches run on any machine.
* Without that, the Windows branch could only ever execute on Windows —
* which is precisely how this survived: the JavaScript suite had never run
* there until 2026-08-20, and when it did, three sink tests failed at once.
*
* The bug: tokenize treated backslash as a POSIX escape unconditionally, so
* every Windows path handed to --exec or --stdin-to lost its separators
* before it was spawned.
*/
it("keeps the separators in a Windows path", () => {
expect(parseCommandSpec(["C:\\Tools\\bin.exe -c"], { windows: true })).toEqual([
"C:\\Tools\\bin.exe",
["-c"],
]);
});
it("keeps them inside quotes too, where a path with spaces has to live", () => {
expect(
parseCommandSpec(['"C:\\Program Files\\tool.exe" --flag'], { windows: true }),
).toEqual(["C:\\Program Files\\tool.exe", ["--flag"]]);
});
it("pins what the POSIX branch does to the same input", () => {
// Not a hypothetical. This is what every Windows user got: a command
// named "C:Program" and an argument called "Filestool.exe".
expect(
parseCommandSpec(['"C:\\Program Files\\tool.exe" --flag'], { windows: false }),
).toEqual(["C:Program Filestool.exe", ["--flag"]]);
});
it("still escapes on POSIX, where a shell would", () => {
expect(parseCommandSpec(["printf a\\ b"], { windows: false })).toEqual([
"printf",
["a b"],
]);
});
it("quoting still groups on Windows", () => {
expect(parseCommandSpec(['sh -c "do the thing"'], { windows: true })).toEqual([
"sh",
["-c", "do the thing"],
]);
});
});