Skip to content

Commit 6b1be11

Browse files
codeslakeclaude
andcommitted
test: give each launcher-spawning file its own TMPDIR
The launcher writes cache-fix-proxy-<port>.sha256 under os.tmpdir() on every spawn, and the tests spawn it with `{ ...process.env }`. So each run left those records in the shared /tmp, and 2,145 had accumulated on this host. Set once per file rather than at each spawn site, because the env is inherited — this also covers sites added later, which is what went wrong the first time. proxy-held-port already had a private TMPDIR at one of its eleven spawn sites, with a comment explaining exactly why ("Asserting a global path can only ever measure the machine's history"); the other ten never got it. Measured with a private TMPDIR per run, before -> after: proxy-held-port 24 -> 0 (34/34) proxy-holder-handover 10 -> 0 (11/11) proxy-wrapper 7 -> 0 (45/45) stdio-epipe-survival 2 -> 0 (4/4) proxy-shutdown-once 1 -> 0 (3/3) proxy-server 0 -> 0 (32/32) consistency, not a fix The counts include the ccf-runsvc-, ccf-wild- and ccf-v6- directories these files also left behind, not only the .sha256 records. proxy-wrapper is a different family from the same cause: runWrapper forks the wrapper with the inherited env and the launcher writes its scratch CA under os.tmpdir(), so seven cache-fix-ca-scratch-* dirs survived each run for the seven days its reaper waits. It goes through that file's own tempDir() registrar rather than a raw mkdtempSync — the file has a meta-assertion requiring exactly that, and it caught the first attempt. In five of the six the cleanup hook is appended at EOF, so it cannot delete the dir out from under a sweep that reaps ports after the cases: node runs root hooks in registration order. proxy-wrapper is the exception — its removal rides the file's existing tempDirs hook near the top, which is harmless there because that is the file's only root hook, but the property does not hold uniformly. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 78ca948 commit 6b1be11

6 files changed

Lines changed: 77 additions & 7 deletions

test/proxy-held-port.test.mjs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,22 @@ import net from "node:net";
55
import { execFileSync, spawn } from "node:child_process";
66
import { fileURLToPath } from "node:url";
77
import { writeFile, rm } from "node:fs/promises";
8-
import { readdirSync, readFileSync, existsSync, mkdirSync, mkdtempSync, writeFileSync, rmSync, utimesSync } from "node:fs";
8+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, utimesSync, writeFileSync } from "node:fs";
99
import { createHash } from "node:crypto";
1010
import { tmpdir, availableParallelism } from "node:os";
1111
import { join, dirname } from "node:path";
1212

1313
import { sourceFingerprintSync } from "../proxy/source-fingerprint.mjs";
1414
import { HOP_ENV, OURS, cmdOf, freePort as takePort, listeners, onPort } from "./proc-helpers.mjs";
1515

16+
// A PRIVATE TMPDIR FOR THE WHOLE FILE. Every launcher spawned here inherits
17+
// process.env, and the launcher writes cache-fix-proxy-<port>.sha256 under
18+
// os.tmpdir() on each spawn — so without this the run leaves those records in
19+
// the shared /tmp, one per spawn. Set once rather than at each spawn site: the
20+
// env is inherited, so this also covers sites added later.
21+
const FILE_TMP = mkdtempSync(join(tmpdir(), "ccf-hp-"));
22+
process.env.TMPDIR = FILE_TMP;
23+
1624
const launcherPath = join(dirname(fileURLToPath(import.meta.url)), "..", "bin", "claude-via-proxy.mjs");
1725

1826
// WHAT A PROBE RESULT MEANS. One definition, because four hand-rolled ones is
@@ -2366,3 +2374,7 @@ after(async () => {
23662374
await new Promise((r) => setTimeout(r, 700));
23672375
}
23682376
});
2377+
2378+
// LAST, so it cannot delete the dir out from under a sweep that reaps ports
2379+
// after the cases: node runs root hooks in registration order.
2380+
after(() => { try { rmSync(FILE_TMP, { recursive: true, force: true }); } catch { /* gone */ } });

test/proxy-holder-handover.test.mjs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,18 @@ import { dirname, join } from "node:path";
88
import { tmpdir } from "node:os";
99
import { createHash } from "node:crypto";
1010
import { EventEmitter } from "node:events";
11-
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
11+
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
1212
import { OURS, cmdOf, freePort as takePort, listeners, onPort } from "./proc-helpers.mjs";
1313

14+
// A PRIVATE TMPDIR FOR THE WHOLE FILE. Every launcher spawned here inherits
15+
// process.env, and the launcher writes cache-fix-proxy-<port>.sha256 under
16+
// os.tmpdir() on each spawn — so without this the run leaves those records in
17+
// the shared /tmp, one per spawn, for every box the suite has ever run on.
18+
// Set once here rather than at each spawn site: the env is inherited, so this
19+
// also covers spawn sites added later.
20+
const FILE_TMP = mkdtempSync(join(tmpdir(), "ccf-hh-"));
21+
process.env.TMPDIR = FILE_TMP;
22+
1423
const launcherPath = join(dirname(fileURLToPath(import.meta.url)), "..", "bin", "claude-via-proxy.mjs");
1524

1625
// Its own file, and that is the point rather than tidiness. This case samples a
@@ -1011,3 +1020,7 @@ describe("openGap identity", () => {
10111020
assert.ok(holder._gap === second, "a late 'error' from the retired gap cleared the live one");
10121021
});
10131022
});
1023+
1024+
// LAST, so it cannot delete the dir out from under a sweep that reaps ports
1025+
// after the cases: node runs root hooks in registration order.
1026+
after(() => { try { rmSync(FILE_TMP, { recursive: true, force: true }); } catch { /* gone */ } });

test/proxy-server.test.mjs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,22 @@ import net from "node:net";
55
import { execFileSync, spawn } from "node:child_process";
66
import { fileURLToPath } from "node:url";
77
import { mkdir, writeFile, rm } from "node:fs/promises";
8-
import { readdirSync, readFileSync } from "node:fs";
8+
import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
99
import { tmpdir } from "node:os";
1010
import { join, dirname } from "node:path";
1111
import { startProxy, upstreamPointsAtSelf } from "../proxy/server.mjs";
1212
import { startWatcher } from "../proxy/watcher.mjs";
1313
import { loadExtensions, getRegistry } from "../proxy/pipeline.mjs";
1414
import { OURS, cmdOf, freePort as takePort, listeners, onPort } from "./proc-helpers.mjs";
1515

16+
// A PRIVATE TMPDIR FOR THE WHOLE FILE. Every launcher spawned here inherits
17+
// process.env, and the launcher writes cache-fix-proxy-<port>.sha256 under
18+
// os.tmpdir() on each spawn — so without this the run leaves those records in
19+
// the shared /tmp, one per spawn. Set once rather than at each spawn site: the
20+
// env is inherited, so this also covers sites added later.
21+
const FILE_TMP = mkdtempSync(join(tmpdir(), "ccf-ps-"));
22+
process.env.TMPDIR = FILE_TMP;
23+
1624
const serverPath = join(dirname(fileURLToPath(import.meta.url)), "..", "proxy", "server.mjs");
1725
const launcherPath = join(dirname(fileURLToPath(import.meta.url)), "..", "bin", "claude-via-proxy.mjs");
1826

@@ -1047,3 +1055,7 @@ describe("client-abandon abort", () => {
10471055
} finally { restore(saved); if (h) await h.close(); }
10481056
});
10491057
});
1058+
1059+
// LAST, so it cannot delete the dir out from under a sweep that reaps ports
1060+
// after the cases: node runs root hooks in registration order.
1061+
after(() => { try { rmSync(FILE_TMP, { recursive: true, force: true }); } catch { /* gone */ } });

test/proxy-shutdown-once.test.mjs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,24 @@
1010
// mutation-checked; it needed isolating, not deleting.
1111
//
1212
// node gives each FILE its own process, which is the whole mechanism.
13-
import { describe, it } from "node:test";
13+
import { after, describe, it } from "node:test";
1414
import assert from "node:assert/strict";
1515
import http from "node:http";
1616
import net from "node:net";
1717
import { spawn } from "node:child_process";
18-
import { readFileSync } from "node:fs";
18+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
1919
import { fileURLToPath } from "node:url";
2020
import { dirname, join } from "node:path";
2121
import { OURS, cmdOf, freePort, listeners } from "./proc-helpers.mjs";
22+
import { tmpdir } from "node:os";
23+
24+
// A PRIVATE TMPDIR FOR THE WHOLE FILE. Every launcher spawned here inherits
25+
// process.env, and the launcher writes cache-fix-proxy-<port>.sha256 under
26+
// os.tmpdir() on each spawn — so without this the run leaves those records in
27+
// the shared /tmp, one per spawn. Set once rather than at each spawn site: the
28+
// env is inherited, so this also covers sites added later.
29+
const FILE_TMP = mkdtempSync(join(tmpdir(), "ccf-so-"));
30+
process.env.TMPDIR = FILE_TMP;
2231

2332
const here = dirname(fileURLToPath(import.meta.url));
2433
const launcherPath = join(here, "..", "bin", "claude-via-proxy.mjs");
@@ -234,3 +243,7 @@ describe("shutdown runs once per stop", () => {
234243
}
235244
});
236245
});
246+
247+
// LAST, so it cannot delete the dir out from under a sweep that reaps ports
248+
// after the cases: node runs root hooks in registration order.
249+
after(() => { try { rmSync(FILE_TMP, { recursive: true, force: true }); } catch { /* gone */ } });

test/proxy-wrapper.test.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ function tempDir(prefix) {
3737
tempDirs.push(d);
3838
return d;
3939
}
40+
// A PRIVATE TMPDIR FOR THE WHOLE FILE, for the same reason the launcher-spawning
41+
// files carry one: runWrapper forks the wrapper with the inherited env, and the
42+
// launcher writes its scratch CA under os.tmpdir() — measured, 7
43+
// cache-fix-ca-scratch-* dirs left in the shared /tmp per run, collected by the
44+
// launcher's own reaper only after seven days.
45+
process.env.TMPDIR = tempDir("ccf-pw-");
46+
4047
after(() => {
4148
for (const d of tempDirs) {
4249
try { rmSync(d, { recursive: true, force: true }); } catch { /* already gone */ }

test/stdio-epipe-survival.test.mjs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,23 @@
2222
// mode, and the holder through BOTH of its dispatch doors — `server` with
2323
// CACHE_FIX_HOLD_PORT=on was the one an earlier fix missed while the other
2424
// passed, so a single holder row would have called that fixed.
25-
import { describe, it } from "node:test";
25+
import { after, describe, it } from "node:test";
2626
import assert from "node:assert/strict";
2727
import net from "node:net";
2828
import { spawn } from "node:child_process";
29-
import { readFileSync } from "node:fs";
29+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3030
import { withDeadline } from "./child-deadline.mjs";
3131
import { fileURLToPath } from "node:url";
3232
import { dirname, join } from "node:path";
33+
import { tmpdir } from "node:os";
34+
35+
// A PRIVATE TMPDIR FOR THE WHOLE FILE. Every launcher spawned here inherits
36+
// process.env, and the launcher writes cache-fix-proxy-<port>.sha256 under
37+
// os.tmpdir() on each spawn — so without this the run leaves those records in
38+
// the shared /tmp, one per spawn. Set once rather than at each spawn site: the
39+
// env is inherited, so this also covers sites added later.
40+
const FILE_TMP = mkdtempSync(join(tmpdir(), "ccf-se-"));
41+
process.env.TMPDIR = FILE_TMP;
3342

3443
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
3544
const reap = (p) => { try { process.kill(-p.pid, "SIGKILL"); } catch {} try { p.kill("SIGKILL"); } catch {} };
@@ -192,3 +201,7 @@ describe("a dead stdio reader does not kill the port's process", () => {
192201
// does this holder have", which is a number on the machine, not a shape in the
193202
// source. `openStandby` states the same identity rule twenty lines below and had
194203
// always followed it; this is the sibling that was missed in the same sweep.
204+
205+
// LAST, so it cannot delete the dir out from under a sweep that reaps ports
206+
// after the cases: node runs root hooks in registration order.
207+
after(() => { try { rmSync(FILE_TMP, { recursive: true, force: true }); } catch { /* gone */ } });

0 commit comments

Comments
 (0)