Public: anyone can read this; the crew can change it.

The check the crew must pass does not pass on main in a crew sandbox #239

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/233-the-check-the-crew-must-pass-does-not-pa → main 3 commits fork

  • Merged. mason/233-the-check-the-crew-must-pass-does-not-pa was merged into main as 6075ea8.

Review

Sign in to review this change.
37 files changed +357 −168
CLAUDE.md +12
@@ -62,6 +62,18 @@ bun run src/main.ts # boots on :3000, data in ./data
6262 - **`git push` is blocked at the shell level.** In tests and scripts that need a
6363 git _client_, drive it through `Bun.spawn(["git", ...])`, never a shell push.
6464 See `test/app.test.ts` / `test/onboarding.test.ts` for the pattern.
65 - **A test's git child gets a built environment, never the host's.** Use
66 `gitClientEnv` (`test/helpers/git-client.ts`) — `...process.env` carries
67 `GIT_CONFIG_COUNT` (git configuration no `GIT_CONFIG_GLOBAL=/dev/null`
68 turns off, and on a crew machine it holds that agent's forge credential)
69 into a child pointed at a forge that never issued it. Same rule as
70 `src/git/env.ts` server-side.
71 - **A test that needs a binary this machine may lack skips, and says so.**
72 `describe.skipIf(missing("/usr/bin/ssh"))` (`test/helpers/tools.ts`) —
73 conditional on the tool, so nothing is lost where the tool is there, and
74 the reason is printed so a skip is never read as a pass.
75 - **`bun run check` is the gate**, and it reads its command from
76 `forge.json`, so what you see is exactly what the push will be judged by.
6577 - **Every non-trivial change ships with a test.** Prefer protocol-level tests
6678 that boot a real app (`createApp` + `serve`) over unit mocks. Adversarial
6779 tests (can this be bypassed?) are worth more than happy-path ones.
README.md +15
@@ -298,8 +298,23 @@ tell, a run and a change each on its own page, and the board.
298298 ```sh
299299 bun test # protocol-level + adversarial suite
300300 bun x tsc --noEmit # zero type errors is the standard
301 bun run check # exactly what the forge runs against every push
301302 ```
302303
304 `bun run check` is the gate: the same command `forge.json` declares, so what
305 it says here is what the deploy will say. Two things it will not do:
306
307 - **It never borrows your git configuration.** A test drives a real `git`
308 against a forge it booted a second ago, and builds that child's
309 environment rather than inheriting yours (`test/helpers/git-client.ts`) —
310 otherwise a `GIT_CONFIG_COUNT` in your shell (a crew run sets one, holding
311 its own forge credential) would send that credential to the test's server
312 and every push in the suite would come back `401`.
313 - **It skips what this machine cannot run, out loud.** The OpenSSH interop
314 tests need `ssh` and `ssh-keygen`; without them the log says `SKIPPING:`
315 and names the tool (`test/helpers/tools.ts`). Install OpenSSH and they run
316 again — the skip is conditional on the binary, never on anything else.
317
303318 ## Shape
304319
305320 ```
forge.json +1 −1
@@ -6,7 +6,7 @@
66 "ready": 90
77 },
88 "check": {
9 "run": "bun x tsc --noEmit && bun test --timeout 60000 test/crew.test.ts test/crew-wrapup.test.ts test/crew-limits.test.ts test/crew-sandbox.test.ts test/steer.test.ts test/landing.test.ts test/decide.test.ts test/ledger.test.ts test/checks.test.ts test/dependencies.test.ts test/autonomy.test.ts test/studio.test.ts test/studio-words.test.ts test/forks.test.ts test/fork-flow.test.ts test/cross-repo-pull.test.ts test/pulls.test.ts test/repos.test.ts test/apps.test.ts test/apps-adversarial.test.ts test/web.test.ts test/git-trust.test.ts",
9 "run": "bun x tsc --noEmit && bun test --timeout 60000 test/crew.test.ts test/crew-wrapup.test.ts test/crew-limits.test.ts test/crew-sandbox.test.ts test/steer.test.ts test/landing.test.ts test/decide.test.ts test/ledger.test.ts test/checks.test.ts test/dependencies.test.ts test/autonomy.test.ts test/studio.test.ts test/studio-words.test.ts test/forks.test.ts test/fork-flow.test.ts test/cross-repo-pull.test.ts test/pulls.test.ts test/repos.test.ts test/apps.test.ts test/apps-adversarial.test.ts test/web.test.ts test/git-trust.test.ts test/helpers.test.ts",
1010 "timeout": 1800
1111 }
1212 }
package.json +2 −1
@@ -7,7 +7,8 @@
77 "dev": "bun run --watch src/main.ts",
88 "start": "bun run src/main.ts",
99 "test": "bun test",
10 "typecheck": "bun x tsc --noEmit"
10 "typecheck": "bun x tsc --noEmit",
11 "check": "bun run scripts/check.ts"
1112 },
1213 "devDependencies": {
1314 "@types/bun": "^1.2.0",
scripts/check.ts added +25
@@ -0,0 +1,25 @@
1 /**
2 * The gate, run the way the forge runs it.
3 *
4 * `forge.json`'s `check.run` is the command every push to a pull must pass.
5 * A crew member has to be able to see it green before pushing, and the only
6 * way that is true is if there is exactly one copy of the command: this
7 * reads it from the manifest and hands it to `/bin/sh -c`, which is what
8 * `apps/process.ts` does with an app's check on the deploy side.
9 */
10
11 const manifest = (await Bun.file(
12 new URL("../forge.json", import.meta.url),
13 ).json()) as { check?: { run?: string } };
14
15 const command = manifest.check?.run;
16 if (typeof command !== "string" || command.trim() === "") {
17 console.error("forge.json declares no check.run");
18 process.exit(2);
19 }
20
21 const proc = Bun.spawn(["/bin/sh", "-c", command], {
22 cwd: new URL("..", import.meta.url).pathname,
23 stdio: ["inherit", "inherit", "inherit"],
24 });
25 process.exit(await proc.exited);
test/app.test.ts +3 −6
@@ -4,17 +4,14 @@ import { tmpdir } from "node:os";
44 import { join } from "node:path";
55 import { createApp, type App } from "../src/app.ts";
66 import { serve, type Servers } from "../src/http/server.ts";
7 import { gitClientEnv } from "./helpers/git-client.ts";
78
8 const clientEnv = {
9 ...process.env,
9 const clientEnv = gitClientEnv({
1010 GIT_AUTHOR_NAME: "Alice",
1111 GIT_AUTHOR_EMAIL: "[email protected]",
1212 GIT_COMMITTER_NAME: "Alice",
1313 GIT_COMMITTER_EMAIL: "[email protected]",
14 GIT_TERMINAL_PROMPT: "0",
15 GIT_CONFIG_GLOBAL: "/dev/null",
16 GIT_CONFIG_SYSTEM: "/dev/null",
17 };
14 });
1815
1916 async function sh(
2017 args: string[],
test/apps-adversarial.test.ts +3 −6
@@ -64,19 +64,16 @@ import {
6464 subscribeRunLog,
6565 } from "../src/runs/log.ts";
6666 import { grantedCookie } from "./helpers/identity.ts";
67 import { gitClientEnv } from "./helpers/git-client.ts";
6768
6869 const DOMAIN = "apps.test";
6970
70 const clientEnv = {
71 ...process.env,
71 const clientEnv = gitClientEnv({
7272 GIT_AUTHOR_NAME: "Agent",
7373 GIT_AUTHOR_EMAIL: "[email protected]",
7474 GIT_COMMITTER_NAME: "Agent",
7575 GIT_COMMITTER_EMAIL: "[email protected]",
76 GIT_TERMINAL_PROMPT: "0",
77 GIT_CONFIG_GLOBAL: "/dev/null",
78 GIT_CONFIG_SYSTEM: "/dev/null",
79 };
76 });
8077
8178 async function sh(args: string[], cwd: string) {
8279 const proc = Bun.spawn(args, {
test/apps-ssh.test.ts +4 −1
@@ -27,11 +27,14 @@ import { headlessTerminal } from "../src/tui/term/terminal.ts";
2727 import { getAppBySlug } from "../src/core/apps.ts";
2828 import { grantedCookie } from "./helpers/identity.ts";
2929 import { settleApp } from "./helpers/tui.ts";
30 import { missing } from "./helpers/tools.ts";
3031
3132 const SSH = "/usr/bin/ssh";
3233 const SSH_KEYGEN = "/usr/bin/ssh-keygen";
34 /** These drive the real OpenSSH client; without it they skip, loudly. */
35 const NO_OPENSSH = missing(SSH, SSH_KEYGEN);
3336
34 describe("a terminal for every app, over ssh", () => {
37 describe.skipIf(NO_OPENSSH)("a terminal for every app, over ssh", () => {
3538 let dataDir: string;
3639 let app: App;
3740 let servers: Servers;
test/apps.test.ts +3 −6
@@ -46,17 +46,14 @@ import { createUser, type User } from "../src/core/users.ts";
4646 import { serve, type Servers } from "../src/http/server.ts";
4747 import { readRunLog } from "../src/runs/log.ts";
4848 import { grantedCookie } from "./helpers/identity.ts";
49 import { gitClientEnv } from "./helpers/git-client.ts";
4950
50 const clientEnv = {
51 ...process.env,
51 const clientEnv = gitClientEnv({
5252 GIT_AUTHOR_NAME: "Agent",
5353 GIT_AUTHOR_EMAIL: "[email protected]",
5454 GIT_COMMITTER_NAME: "Agent",
5555 GIT_COMMITTER_EMAIL: "[email protected]",
56 GIT_TERMINAL_PROMPT: "0",
57 GIT_CONFIG_GLOBAL: "/dev/null",
58 GIT_CONFIG_SYSTEM: "/dev/null",
59 };
56 });
6057
6158 async function sh(args: string[], cwd: string) {
6259 const proc = Bun.spawn(args, {
test/cli-sync.test.ts +4 −4
@@ -17,21 +17,21 @@ import { createApp, type App } from "../src/app.ts";
1717 import { serve, type Servers } from "../src/http/server.ts";
1818 import { hostDir } from "../src/cli/config.ts";
1919 import { changesBetween, type RepoReport, type SyncReport } from "../src/cli/sync.ts";
20 import { gitClientEnv } from "./helpers/git-client.ts";
2021
2122 const CLI = new URL("../src/cli/main.ts", import.meta.url).pathname;
2223
23 const clientEnv = {
24 const clientEnv = gitClientEnv({
2425 GIT_AUTHOR_NAME: "Alice",
2526 GIT_AUTHOR_EMAIL: "[email protected]",
2627 GIT_COMMITTER_NAME: "Alice",
2728 GIT_COMMITTER_EMAIL: "[email protected]",
28 GIT_TERMINAL_PROMPT: "0",
29 };
29 });
3030
3131 async function sh(args: string[], cwd: string): Promise<string> {
3232 const proc = Bun.spawn(args, {
3333 cwd,
34 env: { ...process.env, ...clientEnv },
34 env: clientEnv,
3535 stdout: "pipe",
3636 stderr: "pipe",
3737 });
test/cross-repo-pull.test.ts +3 −6
@@ -4,17 +4,14 @@ import { tmpdir } from "node:os";
44 import { join } from "node:path";
55 import { createApp, type App } from "../src/app.ts";
66 import { serve, type Servers } from "../src/http/server.ts";
7 import { gitClientEnv } from "./helpers/git-client.ts";
78
8 const gitEnv = {
9 ...process.env,
9 const gitEnv = gitClientEnv({
1010 GIT_AUTHOR_NAME: "X",
1111 GIT_AUTHOR_EMAIL: "[email protected]",
1212 GIT_COMMITTER_NAME: "X",
1313 GIT_COMMITTER_EMAIL: "[email protected]",
14 GIT_TERMINAL_PROMPT: "0",
15 GIT_CONFIG_GLOBAL: "/dev/null",
16 GIT_CONFIG_SYSTEM: "/dev/null",
17 };
14 });
1815
1916 describe("cross-repo pull requests (fork → upstream)", () => {
2017 let dataDir: string;
test/diff-edge-cases.test.ts +3 −6
@@ -8,6 +8,7 @@ import { tmpdir } from "node:os";
88 import { join } from "node:path";
99 import { createApp, type App } from "../src/app.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
11 import { gitClientEnv } from "./helpers/git-client.ts";
1112
1213 describe("diff engine + resource edge cases", () => {
1314 let dataDir: string;
@@ -18,16 +19,12 @@ describe("diff engine + resource edge cases", () => {
1819 let cookie = "";
1920 let token = "";
2021
21 const clientEnv = {
22 ...process.env,
22 const clientEnv = gitClientEnv({
2323 GIT_AUTHOR_NAME: "Ada",
2424 GIT_AUTHOR_EMAIL: "[email protected]",
2525 GIT_COMMITTER_NAME: "Ada",
2626 GIT_COMMITTER_EMAIL: "[email protected]",
27 GIT_TERMINAL_PROMPT: "0",
28 GIT_CONFIG_GLOBAL: "/dev/null",
29 GIT_CONFIG_SYSTEM: "/dev/null",
30 };
27 });
3128
3229 async function shOk(args: string[], cwd: string): Promise<string> {
3330 const proc = Bun.spawn(args, {
test/diff.test.ts +3 −6
@@ -21,17 +21,14 @@ import {
2121 stubFor,
2222 type DiffViewOptions,
2323 } from "../src/web/diff-view.ts";
24 import { gitClientEnv } from "./helpers/git-client.ts";
2425
25 const env = {
26 ...process.env,
26 const env = gitClientEnv({
2727 GIT_AUTHOR_NAME: "D",
2828 GIT_AUTHOR_EMAIL: "[email protected]",
2929 GIT_COMMITTER_NAME: "D",
3030 GIT_COMMITTER_EMAIL: "[email protected]",
31 GIT_TERMINAL_PROMPT: "0",
32 GIT_CONFIG_GLOBAL: "/dev/null",
33 GIT_CONFIG_SYSTEM: "/dev/null",
34 };
31 });
3532
3633 async function sh(args: string[], cwd: string): Promise<string> {
3734 const p = Bun.spawn(args, { cwd, env, stdout: "pipe", stderr: "pipe" });
test/forks.test.ts +3 −10
@@ -6,21 +6,14 @@ import { createApp, type App } from "../src/app.ts";
66 import { deleteRepo, getRepo } from "../src/core/repos.ts";
77 import { repoDir } from "../src/git/repos.ts";
88 import { serve, type Servers } from "../src/http/server.ts";
9 import { gitClientEnv } from "./helpers/git-client.ts";
910
10 const gitEnv = {
11 ...process.env,
11 const gitEnv = gitClientEnv({
1212 GIT_AUTHOR_NAME: "F",
1313 GIT_AUTHOR_EMAIL: "[email protected]",
1414 GIT_COMMITTER_NAME: "F",
1515 GIT_COMMITTER_EMAIL: "[email protected]",
16 GIT_TERMINAL_PROMPT: "0",
17 GIT_CONFIG_GLOBAL: "/dev/null",
18 GIT_CONFIG_SYSTEM: "/dev/null",
19 // The same reason the two above are neutered: an ambient GIT_CONFIG_COUNT
20 // (a crew run hands git an `http.extraHeader` for *its* forge) would send
21 // someone else's credential to the test server instead of the URL's.
22 GIT_CONFIG_COUNT: "0",
23 };
16 });
2417
2518 describe("forks", () => {
2619 let dataDir: string;
test/git-local.test.ts +17 −1
@@ -13,6 +13,7 @@ import { tmpdir } from "node:os";
1313 import { join } from "node:path";
1414 import * as git from "../src/git/local.ts";
1515 import { unwrap } from "../src/kernel/result.ts";
16 import { gitClientEnv } from "./helpers/git-client.ts";
1617
1718 const IDENTITY = {
1819 GIT_AUTHOR_NAME: "Ada",
@@ -25,7 +26,7 @@ const IDENTITY = {
2526 async function sh(args: string[], cwd: string): Promise<string> {
2627 const proc = Bun.spawn(["git", ...args], {
2728 cwd,
28 env: { ...process.env, ...IDENTITY, LC_ALL: "C", GIT_TERMINAL_PROMPT: "0" },
29 env: gitClientEnv(IDENTITY),
2930 stdout: "pipe",
3031 stderr: "pipe",
3132 });
@@ -47,12 +48,27 @@ async function newRepo(root: string, name: string): Promise<string> {
4748 }
4849
4950 let root = "";
51 const ambient: Record<string, string | undefined> = {};
5052
5153 beforeAll(async () => {
5254 root = await mkdtemp(join(tmpdir(), "forge-local-"));
55 // `git/local.ts` acts as the person at the keyboard: it inherits their
56 // environment on purpose (their identity, their credential helpers — see
57 // "the environment", below), so a commit *it* writes is authored by
58 // whoever this machine says you are. Here that person is Ada, and a
59 // machine whose shell already names an author (a crew run names its
60 // agent) must not decide it instead.
61 for (const [key, value] of Object.entries(IDENTITY)) {
62 ambient[key] = process.env[key];
63 process.env[key] = value;
64 }
5365 });
5466
5567 afterAll(async () => {
68 for (const [key, value] of Object.entries(ambient)) {
69 if (value === undefined) delete process.env[key];
70 else process.env[key] = value;
71 }
5672 await rm(root, { recursive: true, force: true });
5773 });
5874
test/git-ssh.test.ts +14 −13
@@ -32,10 +32,14 @@ import { runGitService, type GitChannel } from "../src/git/smart-ssh.ts";
3232 import { serveForgeSsh } from "../src/tui/ssh.ts";
3333 import type { SshServer } from "../src/ssh/server.ts";
3434 import { serve, type Servers } from "../src/http/server.ts";
35 import { gitClientEnv } from "./helpers/git-client.ts";
36 import { missing } from "./helpers/tools.ts";
3537
3638 setLogLevel("error");
3739
3840 const SSH_KEYGEN = "/usr/bin/ssh-keygen";
41 /** The client half is the real OpenSSH; without it these skip, loudly. */
42 const NO_OPENSSH = missing("/usr/bin/ssh", SSH_KEYGEN);
3943
4044 interface Run {
4145 code: number;
@@ -43,7 +47,7 @@ interface Run {
4347 err: string;
4448 }
4549
46 describe("git over ssh", () => {
50 describe.skipIf(NO_OPENSSH)("git over ssh", () => {
4751 let dataDir: string;
4852 let keyDir: string;
4953 let work: string;
@@ -53,17 +57,14 @@ describe("git over ssh", () => {
5357 let alice: User;
5458 let bob: User;
5559
56 const env = (key: string): Record<string, string> => ({
57 ...(process.env as Record<string, string>),
58 GIT_SSH_COMMAND: `/usr/bin/ssh -i ${key} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o BatchMode=yes -o LogLevel=ERROR -o ConnectTimeout=5`,
59 GIT_AUTHOR_NAME: "Tester",
60 GIT_AUTHOR_EMAIL: "[email protected]",
61 GIT_COMMITTER_NAME: "Tester",
62 GIT_COMMITTER_EMAIL: "[email protected]",
63 GIT_TERMINAL_PROMPT: "0",
64 GIT_CONFIG_GLOBAL: "/dev/null",
65 GIT_CONFIG_SYSTEM: "/dev/null",
66 });
60 const env = (key: string): Record<string, string> =>
61 gitClientEnv({
62 GIT_SSH_COMMAND: `/usr/bin/ssh -i ${key} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o BatchMode=yes -o LogLevel=ERROR -o ConnectTimeout=5`,
63 GIT_AUTHOR_NAME: "Tester",
64 GIT_AUTHOR_EMAIL: "[email protected]",
65 GIT_COMMITTER_NAME: "Tester",
66 GIT_COMMITTER_EMAIL: "[email protected]",
67 });
6768
6869 /** `git push` is blocked at the shell level, so every client call spawns. */
6970 async function git(
@@ -310,7 +311,7 @@ describe("git over ssh", () => {
310311 * real ssh; what this isolates is the lifetime rule those tests cannot make
311312 * visible — whether the child outlives a peer that walked away.
312313 */
313 describe("a git service outlives neither the channel nor the peer", () => {
314 describe.skipIf(NO_OPENSSH)("a git service outlives neither the channel nor the peer", () => {
314315 let dataDir: string;
315316 let forge: Forge;
316317 let dir: string;
test/git.test.ts +3 −6
@@ -27,18 +27,15 @@ import {
2727 serviceRpc,
2828 } from "../src/git/smart-http.ts";
2929 import { unwrap } from "../src/kernel/result.ts";
30 import { gitClientEnv } from "./helpers/git-client.ts";
3031
3132 // Client-side git for driving the server under test.
32 const clientEnv = {
33 ...process.env,
33 const clientEnv = gitClientEnv({
3434 GIT_AUTHOR_NAME: "Test",
3535 GIT_AUTHOR_EMAIL: "[email protected]",
3636 GIT_COMMITTER_NAME: "Test",
3737 GIT_COMMITTER_EMAIL: "[email protected]",
38 GIT_TERMINAL_PROMPT: "0",
39 GIT_CONFIG_GLOBAL: "/dev/null",
40 GIT_CONFIG_SYSTEM: "/dev/null",
41 };
38 });
4239
4340 async function sh(args: string[], cwd: string): Promise<string> {
4441 const proc = Bun.spawn(args, {
test/hardening.test.ts +3 −6
@@ -15,17 +15,14 @@ import { reconcile } from "../src/core/reconcile.ts";
1515 import { repoDir } from "../src/git/repos.ts";
1616 import { serve, type Servers } from "../src/http/server.ts";
1717 import { renderMarkdown } from "../src/web/markdown.ts";
18 import { gitClientEnv } from "./helpers/git-client.ts";
1819
19 const gitEnv = {
20 ...process.env,
20 const gitEnv = gitClientEnv({
2121 GIT_AUTHOR_NAME: "T",
2222 GIT_AUTHOR_EMAIL: "[email protected]",
2323 GIT_COMMITTER_NAME: "T",
2424 GIT_COMMITTER_EMAIL: "[email protected]",
25 GIT_TERMINAL_PROMPT: "0",
26 GIT_CONFIG_GLOBAL: "/dev/null",
27 GIT_CONFIG_SYSTEM: "/dev/null",
28 };
25 });
2926
3027 async function shOk(args: string[], cwd: string): Promise<void> {
3128 const proc = Bun.spawn(args, {
test/helpers.test.ts added +77
@@ -0,0 +1,77 @@
1 /**
2 * The two helpers the suite's own health rests on: the environment a git
3 * client under test runs in, and whether a tool this machine would need is
4 * here. Both exist because of a run that failed for the machine's reasons
5 * and was read as the change's fault (#233).
6 */
7
8 import { describe, expect, test } from "bun:test";
9 import { mkdtemp, rm } from "node:fs/promises";
10 import { tmpdir } from "node:os";
11 import { join } from "node:path";
12 import { gitClientEnv } from "./helpers/git-client.ts";
13 import { missing } from "./helpers/tools.ts";
14
15 describe("a git client's environment under test", () => {
16 test("carries no ambient git configuration, whatever the host sets", () => {
17 const env = gitClientEnv();
18 // The crew's own shell sets exactly this: configuration in variables,
19 // which `GIT_CONFIG_GLOBAL=/dev/null` cannot switch off, carrying an
20 // Authorization header for *its* forge. None of it comes through.
21 expect(env["GIT_CONFIG_COUNT"]).toBeUndefined();
22 expect(env["GIT_CONFIG_KEY_0"]).toBeUndefined();
23 expect(env["GIT_CONFIG_VALUE_0"]).toBeUndefined();
24 expect(env["GIT_CONFIG_PARAMETERS"]).toBeUndefined();
25 expect(env["GIT_CONFIG_GLOBAL"]).toBe("/dev/null");
26 expect(env["GIT_CONFIG_SYSTEM"]).toBe("/dev/null");
27 expect(env["GIT_TERMINAL_PROMPT"]).toBe("0");
28 expect(env["PATH"]).toBe(process.env["PATH"] ?? "/usr/bin:/bin");
29 // The test's own additions win.
30 expect(gitClientEnv({ GIT_AUTHOR_NAME: "Ada" })["GIT_AUTHOR_NAME"]).toBe(
31 "Ada",
32 );
33 });
34
35 test("a push to a URL that names its credentials is the one git sends", async () => {
36 // The end of the story above, without a server: a config header set the
37 // way the crew's shell sets it would reach the remote instead of the
38 // URL's credentials. `git config --list` is what git would obey.
39 const dir = await mkdtemp(join(tmpdir(), "forge-git-env-"));
40 try {
41 const listed = async (env: Record<string, string>): Promise<string> => {
42 const proc = Bun.spawn(["git", "config", "--list"], {
43 cwd: dir,
44 env,
45 stdout: "pipe",
46 stderr: "ignore",
47 });
48 const [out] = await Promise.all([
49 new Response(proc.stdout).text(),
50 proc.exited,
51 ]);
52 return out;
53 };
54 const ambient = {
55 ...gitClientEnv(),
56 GIT_CONFIG_COUNT: "1",
57 GIT_CONFIG_KEY_0: "http.extraHeader",
58 GIT_CONFIG_VALUE_0: "Authorization: Basic c29tZWJvZHk6ZWxzZQ==",
59 };
60 expect(await listed(ambient)).toContain("http.extraheader=");
61 expect(await listed(gitClientEnv())).not.toContain("http.extraheader=");
62 } finally {
63 await rm(dir, { recursive: true, force: true });
64 }
65 });
66 });
67
68 describe("whether a tool is on this machine", () => {
69 test("a tool that is here is never skipped; one that is not, is", () => {
70 expect(missing("/bin/sh")).toBe(false);
71 expect(missing("sh")).toBe(false);
72 expect(missing("no-such-tool-on-any-machine")).toBe(true);
73 expect(missing("/usr/bin/no-such-tool")).toBe(true);
74 // Any one absent is enough to skip.
75 expect(missing("sh", "no-such-tool-on-any-machine")).toBe(true);
76 });
77 });
test/helpers/git-client.ts added +49
@@ -0,0 +1,49 @@
1 /**
2 * The environment a test's `git` *client* runs in.
3 *
4 * Server-side git already refuses to inherit the host (`src/git/env.ts`:
5 * "Nothing from the host leaks in"). A test drives a real git binary against
6 * a forge it booted a second ago, so it needs the same discipline, and for a
7 * sharper reason: git reads configuration from the environment as well as
8 * from files. `GIT_CONFIG_COUNT` with `GIT_CONFIG_KEY_n`/`GIT_CONFIG_VALUE_n`
9 * is configuration no `GIT_CONFIG_GLOBAL=/dev/null` can switch off.
10 *
11 * On a crew machine that pair is set: the agent's shell carries
12 * `http.extraHeader=Authorization: Basic <the agent's forge token>` so its
13 * own pushes authenticate. A test that spreads `process.env` into its git
14 * child sends that header to *its* forge, where the credential means
15 * nothing — and an explicit `Authorization` header wins over the
16 * `user:token` in the clone URL, so the push comes back `401 auth required`
17 * and the test fails for reasons that have nothing to do with the code.
18 *
19 * So: build the child environment, never inherit it.
20 */
21
22 /** Author/committer identity every test commit is made with. */
23 const IDENTITY = {
24 GIT_AUTHOR_NAME: "Dev",
25 GIT_AUTHOR_EMAIL: "[email protected]",
26 GIT_COMMITTER_NAME: "Dev",
27 GIT_COMMITTER_EMAIL: "[email protected]",
28 } as const;
29
30 /**
31 * A deterministic environment for a git client under test: `PATH` and
32 * nothing else from the host, no global or system config, no prompt, and a
33 * fixed identity. `extra` is the test's own additions — a clone URL's
34 * askpass, `GIT_SSH_COMMAND`, a different author — and wins.
35 */
36 export function gitClientEnv(
37 extra: Record<string, string> = {},
38 ): Record<string, string> {
39 return {
40 PATH: process.env["PATH"] ?? "/usr/bin:/bin",
41 HOME: "/dev/null",
42 GIT_CONFIG_GLOBAL: "/dev/null",
43 GIT_CONFIG_SYSTEM: "/dev/null",
44 GIT_TERMINAL_PROMPT: "0",
45 LC_ALL: "C",
46 ...IDENTITY,
47 ...extra,
48 };
49 }
test/helpers/tools.ts added +38
@@ -0,0 +1,38 @@
1 /**
2 * Is this tool on this machine?
3 *
4 * A few test files drive a real binary the forge itself never needs —
5 * OpenSSH's `ssh` and `ssh-keygen`, to prove our SSH server interoperates
6 * with the client people actually use. A machine without them cannot run
7 * those tests, and a test that cannot run should say so and skip: failing
8 * is a lie about the code, and skipping quietly is a lie about the run.
9 *
10 * So `missing` is a condition for `describe.skipIf`/`test.skipIf`, and it
11 * prints the reason once per tool. Never skip unconditionally: on a machine
12 * that has the tool, every one of these tests still runs.
13 */
14
15 import { existsSync } from "node:fs";
16
17 /** A bare name is looked up on PATH; an absolute path is looked at. */
18 function here(tool: string): boolean {
19 return tool.startsWith("/") ? existsSync(tool) : Bun.which(tool) !== null;
20 }
21
22 const announced = new Set<string>();
23
24 /**
25 * True when any of `tools` is absent — and says which, on stdout, so the
26 * skip is visible in the log and nobody reads it as a pass.
27 */
28 export function missing(...tools: string[]): boolean {
29 const absent = tools.filter((tool) => !here(tool));
30 for (const tool of absent) {
31 if (announced.has(tool)) continue;
32 announced.add(tool);
33 console.log(
34 `SKIPPING: ${tool} is not installed here, so the tests that drive it cannot run on this machine`,
35 );
36 }
37 return absent.length > 0;
38 }
test/keys.test.ts +5 −1
@@ -19,6 +19,10 @@ import {
1919 } from "../src/core/keys.ts";
2020 import type { User } from "../src/core/users.ts";
2121 import { serve, type Servers } from "../src/http/server.ts";
22 import { missing } from "./helpers/tools.ts";
23
24 /** Real keys come from real ssh-keygen; without it these skip, loudly. */
25 const NO_KEYGEN = missing("ssh-keygen");
2226
2327 async function keygen(dir: string, name: string, args: string[]) {
2428 const file = join(dir, name);
@@ -34,7 +38,7 @@ async function keygen(dir: string, name: string, args: string[]) {
3438 return (await readFile(`${file}.pub`, "utf8")).trim();
3539 }
3640
37 describe("ssh keys", () => {
41 describe.skipIf(NO_KEYGEN)("ssh keys", () => {
3842 let dataDir: string;
3943 let keyDir: string;
4044 let app: App;
test/onboarding.test.ts +3 −6
@@ -4,17 +4,14 @@ import { tmpdir } from "node:os";
44 import { join, relative } from "node:path";
55 import { createApp, type App } from "../src/app.ts";
66 import { serve, type Servers } from "../src/http/server.ts";
7 import { gitClientEnv } from "./helpers/git-client.ts";
78
8 const gitEnv = {
9 ...process.env,
10 GIT_CONFIG_GLOBAL: "/dev/null",
11 GIT_CONFIG_SYSTEM: "/dev/null",
12 GIT_TERMINAL_PROMPT: "0",
9 const gitEnv = gitClientEnv({
1310 GIT_AUTHOR_NAME: "N",
1411 GIT_AUTHOR_EMAIL: "[email protected]",
1512 GIT_COMMITTER_NAME: "N",
1613 GIT_COMMITTER_EMAIL: "[email protected]",
17 };
14 });
1815
1916 describe("onboarding: a person with just a login", () => {
2017 let dataDir: string;
test/orgs.test.ts +3 −6
@@ -18,17 +18,14 @@ import { canRead, canWrite, canAdmin } from "../src/core/repos.ts";
1818 import { createRepo, getRepo } from "../src/core/repos.ts";
1919 import { createUser, getUserByName } from "../src/core/users.ts";
2020 import { serve, type Servers } from "../src/http/server.ts";
21 import { gitClientEnv } from "./helpers/git-client.ts";
2122
22 const gitEnv = {
23 ...process.env,
23 const gitEnv = gitClientEnv({
2424 GIT_AUTHOR_NAME: "O",
2525 GIT_AUTHOR_EMAIL: "[email protected]",
2626 GIT_COMMITTER_NAME: "O",
2727 GIT_COMMITTER_EMAIL: "[email protected]",
28 GIT_TERMINAL_PROMPT: "0",
29 GIT_CONFIG_GLOBAL: "/dev/null",
30 GIT_CONFIG_SYSTEM: "/dev/null",
31 };
28 });
3229
3330 describe("org & team core", () => {
3431 let dataDir: string;
test/origins-security-push.test.ts +3 −6
@@ -25,6 +25,7 @@ import {
2525 serveFakeForge,
2626 type FakeForge,
2727 } from "./fake-origin.ts";
28 import { gitClientEnv } from "./helpers/git-client.ts";
2829
2930 /**
3031 * Adversarial push-through. The pusher is the attacker: a local writer on a
@@ -63,17 +64,13 @@ const read = async <T>(response: Response): Promise<T> =>
6364 // Captured before the PATH wrapper goes in, so the *client* git in these
6465 // tests is the real one and only the forge's spawns are recorded.
6566 const ORIGINAL_PATH = process.env.PATH ?? "/usr/bin:/bin";
66 const clientEnv = {
67 ...process.env,
67 const clientEnv = gitClientEnv({
6868 PATH: ORIGINAL_PATH,
6969 GIT_AUTHOR_NAME: "Alice",
7070 GIT_AUTHOR_EMAIL: "[email protected]",
7171 GIT_COMMITTER_NAME: "Alice",
7272 GIT_COMMITTER_EMAIL: "[email protected]",
73 GIT_TERMINAL_PROMPT: "0",
74 GIT_CONFIG_GLOBAL: "/dev/null",
75 GIT_CONFIG_SYSTEM: "/dev/null",
76 };
73 });
7774
7875 async function git(
7976 args: string[],
test/parity.test.ts +3 −6
@@ -15,17 +15,14 @@ import { listEvents } from "../src/core/events.ts";
1515 import { createSession, pruneSessions } from "../src/core/sessions.ts";
1616 import { getRepo } from "../src/core/repos.ts";
1717 import { serve, type Servers } from "../src/http/server.ts";
18 import { gitClientEnv } from "./helpers/git-client.ts";
1819
19 const gitEnv = {
20 ...process.env,
20 const gitEnv = gitClientEnv({
2121 GIT_AUTHOR_NAME: "A",
2222 GIT_AUTHOR_EMAIL: "[email protected]",
2323 GIT_COMMITTER_NAME: "A",
2424 GIT_COMMITTER_EMAIL: "[email protected]",
25 GIT_TERMINAL_PROMPT: "0",
26 GIT_CONFIG_GLOBAL: "/dev/null",
27 GIT_CONFIG_SYSTEM: "/dev/null",
28 };
25 });
2926
3027 async function git(args: string[], cwd: string): Promise<string> {
3128 const p = Bun.spawn(["git", ...args], {
test/pulls.test.ts +2 −10
@@ -4,17 +4,9 @@ import { tmpdir } from "node:os";
44 import { join } from "node:path";
55 import { createApp, type App } from "../src/app.ts";
66 import { serve, type Servers } from "../src/http/server.ts";
7 import { gitClientEnv } from "./helpers/git-client.ts";
78
8 const gitEnv = {
9 ...process.env,
10 GIT_AUTHOR_NAME: "Dev",
11 GIT_AUTHOR_EMAIL: "[email protected]",
12 GIT_COMMITTER_NAME: "Dev",
13 GIT_COMMITTER_EMAIL: "[email protected]",
14 GIT_TERMINAL_PROMPT: "0",
15 GIT_CONFIG_GLOBAL: "/dev/null",
16 GIT_CONFIG_SYSTEM: "/dev/null",
17 };
9 const gitEnv = gitClientEnv();
1810
1911 describe("pull requests end to end", () => {
2012 let dataDir: string;
test/push-through.test.ts +3 −6
@@ -12,6 +12,7 @@ import {
1212 serveFakeForge,
1313 type FakeForge,
1414 } from "./fake-origin.ts";
15 import { gitClientEnv } from "./helpers/git-client.ts";
1516
1617 /**
1718 * Push-through: a `git push` to a mirror lands upstream, or it does not land
@@ -30,16 +31,12 @@ const basic = (user: string, secret: string): string =>
3031 const read = async <T>(response: Response): Promise<T> =>
3132 (await response.json()) as T;
3233
33 const clientEnv = {
34 ...process.env,
34 const clientEnv = gitClientEnv({
3535 GIT_AUTHOR_NAME: "Alice",
3636 GIT_AUTHOR_EMAIL: "[email protected]",
3737 GIT_COMMITTER_NAME: "Alice",
3838 GIT_COMMITTER_EMAIL: "[email protected]",
39 GIT_TERMINAL_PROMPT: "0",
40 GIT_CONFIG_GLOBAL: "/dev/null",
41 GIT_CONFIG_SYSTEM: "/dev/null",
42 };
39 });
4340
4441 async function git(
4542 args: string[],
test/reviews.test.ts +3 −6
@@ -5,17 +5,14 @@ import { join } from "node:path";
55 import { createApp, type App } from "../src/app.ts";
66 import { serve, type Servers } from "../src/http/server.ts";
77 import { hashLine } from "../src/core/reviews.ts";
8 import { gitClientEnv } from "./helpers/git-client.ts";
89
9 const gitEnv = {
10 ...process.env,
10 const gitEnv = gitClientEnv({
1111 GIT_AUTHOR_NAME: "Rev",
1212 GIT_AUTHOR_EMAIL: "[email protected]",
1313 GIT_COMMITTER_NAME: "Rev",
1414 GIT_COMMITTER_EMAIL: "[email protected]",
15 GIT_TERMINAL_PROMPT: "0",
16 GIT_CONFIG_GLOBAL: "/dev/null",
17 GIT_CONFIG_SYSTEM: "/dev/null",
18 };
15 });
1916
2017 describe("review comments on diff lines", () => {
2118 let dataDir: string;
test/security-merge.test.ts +3 −6
@@ -14,17 +14,14 @@ import { tmpdir } from "node:os";
1414 import { join } from "node:path";
1515 import { createApp, type App } from "../src/app.ts";
1616 import { serve, type Servers } from "../src/http/server.ts";
17 import { gitClientEnv } from "./helpers/git-client.ts";
1718
18 const gitEnv = {
19 ...process.env,
19 const gitEnv = gitClientEnv({
2020 GIT_AUTHOR_NAME: "Dev",
2121 GIT_AUTHOR_EMAIL: "[email protected]",
2222 GIT_COMMITTER_NAME: "Dev",
2323 GIT_COMMITTER_EMAIL: "[email protected]",
24 GIT_TERMINAL_PROMPT: "0",
25 GIT_CONFIG_GLOBAL: "/dev/null",
26 GIT_CONFIG_SYSTEM: "/dev/null",
27 };
24 });
2825
2926 describe("security: read-only token scope bypass on web mutation routes", () => {
3027 let dataDir: string;
test/security-xss.test.ts +3 −6
@@ -11,17 +11,14 @@ import { tmpdir } from "node:os";
1111 import { join } from "node:path";
1212 import { createApp, type App } from "../src/app.ts";
1313 import { serve, type Servers } from "../src/http/server.ts";
14 import { gitClientEnv } from "./helpers/git-client.ts";
1415
15 const clientEnv = {
16 ...process.env,
16 const clientEnv = gitClientEnv({
1717 GIT_AUTHOR_NAME: "Alice",
1818 GIT_AUTHOR_EMAIL: "[email protected]",
1919 GIT_COMMITTER_NAME: "Alice",
2020 GIT_COMMITTER_EMAIL: "[email protected]",
21 GIT_TERMINAL_PROMPT: "0",
22 GIT_CONFIG_GLOBAL: "/dev/null",
23 GIT_CONFIG_SYSTEM: "/dev/null",
24 };
21 });
2522
2623 async function sh(args: string[], cwd: string) {
2724 const proc = Bun.spawn(args, {
test/ssh-app.test.ts +5 −2
@@ -20,12 +20,15 @@ import { MSG, Writer } from "../src/ssh/wire.ts";
2020 import { createLog } from "../src/kernel/log.ts";
2121 import { serveForgeSsh } from "../src/tui/ssh.ts";
2222 import type { SshServer } from "../src/ssh/server.ts";
23 import { missing } from "./helpers/tools.ts";
2324
2425 setLogLevel("error");
2526 const log = createLog("ssh-app-test");
2627
2728 const SSH = "/usr/bin/ssh";
2829 const SSH_KEYGEN = "/usr/bin/ssh-keygen";
30 /** These drive the real OpenSSH client; without it they skip, loudly. */
31 const NO_OPENSSH = missing(SSH, SSH_KEYGEN);
2932
3033 /** Strip SGR, cursor moves and the rest of what a TUI paints. */
3134 function plain(bytes: Uint8Array): string {
@@ -87,7 +90,7 @@ async function connect(
8790 return { out: plain(await stdout), code };
8891 }
8992
90 describe("ssh into the forge", () => {
93 describe.skipIf(NO_OPENSSH)("ssh into the forge", () => {
9194 let dataDir: string;
9295 let keyDir: string;
9396 let forge: Forge;
@@ -151,7 +154,7 @@ describe("ssh into the forge", () => {
151154 });
152155 });
153156
154 describe("channel write backpressure", () => {
157 describe.skipIf(NO_OPENSSH)("channel write backpressure", () => {
155158 /** A channel whose peer granted a tiny window — the clone case, in small. */
156159 function harness(remoteWindow: number) {
157160 const sent: Uint8Array[] = [];
test/ssh.test.ts +18 −11
@@ -24,9 +24,12 @@ import {
2424 type SshServerOptions,
2525 type SshSession,
2626 } from "../src/ssh/server.ts";
27 import { missing } from "./helpers/tools.ts";
2728
2829 const SSH = "/usr/bin/ssh";
2930 const SSH_KEYGEN = "/usr/bin/ssh-keygen";
31 /** These drive the real OpenSSH client; without it they skip, loudly. */
32 const NO_OPENSSH = missing(SSH, SSH_KEYGEN);
3033 const log = createLog("ssh-test");
3134
3235 let root = "";
@@ -354,6 +357,10 @@ function probe(
354357 }
355358
356359 beforeAll(async () => {
360 // This hook belongs to the file, not to a describe, so it runs even when
361 // every case in it is skipped: with no ssh-keygen there are no keys to
362 // make, and nothing that wanted them will run.
363 if (NO_OPENSSH) return;
357364 root = mkdtempSync(join(tmpdir(), "forge-ssh-"));
358365 const specs: [string, string[]][] = [
359366 ["ed25519", ["-t", "ed25519"]],
@@ -377,7 +384,7 @@ afterAll(() => {
377384 if (root) rmSync(root, { recursive: true, force: true });
378385 });
379386
380 describe("ssh drain", () => {
387 describe.skipIf(NO_OPENSSH)("ssh drain", () => {
381388 test("a draining server keeps who is connected, answers nobody new, and ends when the last leaves", async () => {
382389 const server = await start({ onSession: () => {} });
383390 const opened = await Bun.connect({
@@ -417,7 +424,7 @@ describe("ssh drain", () => {
417424 });
418425 });
419426
420 describe("ssh exec sessions", () => {
427 describe.skipIf(NO_OPENSSH)("ssh exec sessions", () => {
421428 test("runs a command, returns output and an exit status", async () => {
422429 let seen: SshSession | undefined;
423430 const server = await start({
@@ -508,7 +515,7 @@ describe("ssh exec sessions", () => {
508515 });
509516 });
510517
511 describe("ssh shell sessions", () => {
518 describe.skipIf(NO_OPENSSH)("ssh shell sessions", () => {
512519 test("pty-req, echo, and EOF ending the session", async () => {
513520 let seen: SshSession | undefined;
514521 let ended = false;
@@ -584,7 +591,7 @@ describe("ssh shell sessions", () => {
584591 });
585592 });
586593
587 describe("ssh publickey authentication", () => {
594 describe.skipIf(NO_OPENSSH)("ssh publickey authentication", () => {
588595 const cases: [string, string][] = [
589596 ["ed25519", "ssh-ed25519"],
590597 ["ecdsa", "ecdsa-sha2-nistp256"],
@@ -696,7 +703,7 @@ describe("ssh publickey authentication", () => {
696703 });
697704 });
698705
699 describe("ssh flow control", () => {
706 describe.skipIf(NO_OPENSSH)("ssh flow control", () => {
700707 const SIZE = 3 * 1024 * 1024;
701708
702709 test("server writes 3 MiB past the peer window", async () => {
@@ -738,7 +745,7 @@ describe("ssh flow control", () => {
738745 }, 30_000);
739746 });
740747
741 describe("ssh rekey", () => {
748 describe.skipIf(NO_OPENSSH)("ssh rekey", () => {
742749 test("survives client-initiated rekey with RekeyLimit=64K", async () => {
743750 const size = 1024 * 1024;
744751 const down = payload(size);
@@ -775,7 +782,7 @@ describe("ssh rekey", () => {
775782 }, 60_000);
776783 });
777784
778 describe("ssh resilience", () => {
785 describe.skipIf(NO_OPENSSH)("ssh resilience", () => {
779786 test("garbage after the version line is disconnected, server keeps serving", async () => {
780787 const server = await start({
781788 onSession(session) {
@@ -853,7 +860,7 @@ describe("ssh resilience", () => {
853860 }
854861 });
855862
856 describe("ssh host key", () => {
863 describe.skipIf(NO_OPENSSH)("ssh host key", () => {
857864 test("persists across restarts and matches ssh-keygen", async () => {
858865 const dir = mkdtempSync(join(root, "persist-"));
859866 const first = await start({ hostKeyDir: dir, onSession: (s) => s.exit(0) });
@@ -877,7 +884,7 @@ describe("ssh host key", () => {
877884 });
878885 });
879886
880 describe("ssh key exchange discipline", () => {
887 describe.skipIf(NO_OPENSSH)("ssh key exchange discipline", () => {
881888 test("a client that never sends NEWKEYS cannot smuggle USERAUTH_REQUEST through in plaintext", async () => {
882889 let authCalls = 0;
883890 let sessionCalls = 0;
@@ -938,7 +945,7 @@ describe("ssh key exchange discipline", () => {
938945 * the connection layer directly with hand-built wire payloads so the
939946 * dimension, resize and rejection paths are still covered.
940947 */
941 describe("ssh channel layer", () => {
948 describe.skipIf(NO_OPENSSH)("ssh channel layer", () => {
942949 function harness() {
943950 const sent: Uint8Array[] = [];
944951 let session: SshSession | undefined;
@@ -1110,7 +1117,7 @@ describe("ssh channel layer", () => {
11101117 });
11111118 });
11121119
1113 describe("ssh fuzzing", () => {
1120 describe.skipIf(NO_OPENSSH)("ssh fuzzing", () => {
11141121 test("random garbage on many connections never takes the server down", async () => {
11151122 const server = await start({
11161123 // Random bytes are often merely *incomplete* rather than illegal — a
test/studio.test.ts +9 −1
@@ -5014,7 +5014,15 @@ describe("a person's own change ships from its own studio page", () => {
50145014 ).text();
50155015 expect(ownerPage).toContain("<h2>Decide</h2>");
50165016 expect(ownerPage).toContain("Ship");
5017 expect(ownerPage).toContain("The check is green.");
5017 // The words a person's own green change is offered in are `asks.ts`'s
5018 // (#224: a green change asks to be looked at whoever opened it, and the
5019 // card says so without pretending somebody finished it for them). The
5020 // `own` fallback in `studio/change.ts` — "The check is green." — is what
5021 // an autonomous repository's own pull still gets, where the asks panel
5022 // offers nothing.
5023 expect(ownerPage).toContain(
5024 "is green — ready for a look. Ship it, or say what is off.",
5025 );
50185026
50195027 const shipped = await post(
50205028 `/alice/garden/pulls/${pull.number}/ship`,
test/tui-local.test.ts +3 −4
@@ -19,18 +19,17 @@ import {
1919 } from "../src/tui/term/terminal.ts";
2020 import { createLocalApp } from "../src/tui/local/app.ts";
2121 import { localBusy, localIdle } from "../src/tui/local/common.ts";
22 import { gitClientEnv } from "./helpers/git-client.ts";
2223
2324 async function sh(args: string[], cwd: string): Promise<void> {
2425 const proc = Bun.spawn(["git", ...args], {
2526 cwd,
26 env: {
27 ...process.env,
27 env: gitClientEnv({
2828 GIT_AUTHOR_NAME: "Ada",
2929 GIT_AUTHOR_EMAIL: "[email protected]",
3030 GIT_COMMITTER_NAME: "Ada",
3131 GIT_COMMITTER_EMAIL: "[email protected]",
32 LC_ALL: "C",
33 },
32 }),
3433 stdout: "pipe",
3534 stderr: "pipe",
3635 });
test/web-ui.test.ts +3 −6
@@ -17,6 +17,7 @@ import { createApp, type App } from "../src/app.ts";
1717 import { getRepo } from "../src/core/repos.ts";
1818 import { listLabels, labelsFor, getIssue } from "../src/core/issues.ts";
1919 import { serve, type Servers } from "../src/http/server.ts";
20 import { gitClientEnv } from "./helpers/git-client.ts";
2021
2122 const form = (fields: Record<string, string>) => new URLSearchParams(fields);
2223
@@ -30,16 +31,12 @@ describe("web ui", () => {
3031 let stranger = "";
3132 let pullNumber = 0;
3233
33 const gitEnv = {
34 ...process.env,
34 const gitEnv = gitClientEnv({
3535 GIT_AUTHOR_NAME: "Mel",
3636 GIT_AUTHOR_EMAIL: "[email protected]",
3737 GIT_COMMITTER_NAME: "Mel",
3838 GIT_COMMITTER_EMAIL: "[email protected]",
39 GIT_TERMINAL_PROMPT: "0",
40 GIT_CONFIG_GLOBAL: "/dev/null",
41 GIT_CONFIG_SYSTEM: "/dev/null",
42 };
39 });
4340
4441 async function git(args: string[], cwd: string): Promise<number> {
4542 const proc = Bun.spawn(["git", ...args], {
test/web.test.ts +3 −6
@@ -16,6 +16,7 @@ import { renderMarkdown } from "../src/web/markdown.ts";
1616 import { highlightAsset } from "../src/web/asset.ts";
1717 import { securityFloor } from "../src/http/respond.ts";
1818 import { THEME_NAMES } from "../src/design/themes.ts";
19 import { gitClientEnv } from "./helpers/git-client.ts";
1920
2021 describe("html template", () => {
2122 test("escapes interpolations by default", () => {
@@ -285,16 +286,12 @@ describe("web pages end to end", () => {
285286 let cookie = "";
286287 let token = "";
287288
288 const clientEnv = {
289 ...process.env,
289 const clientEnv = gitClientEnv({
290290 GIT_AUTHOR_NAME: "Ada",
291291 GIT_AUTHOR_EMAIL: "[email protected]",
292292 GIT_COMMITTER_NAME: "Ada",
293293 GIT_COMMITTER_EMAIL: "[email protected]",
294 GIT_TERMINAL_PROMPT: "0",
295 GIT_CONFIG_GLOBAL: "/dev/null",
296 GIT_CONFIG_SYSTEM: "/dev/null",
297 };
294 });
298295
299296 async function shOk(args: string[], cwd: string): Promise<string> {
300297 const proc = Bun.spawn(args, {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs