Skip to content

Commit 2c0ea06

Browse files
codeslakeclaude
andcommitted
fix(launcher): base64 quanta, full label grammar, explicit launcher signal
A Codex review pass found three more defects. All three reproduced against a real NODE_EXTRA_CA_CERTS handshake before fixing; the two P1s were false accepts of the same class the previous commits were fixing. - Base64 was checked as an ALPHABET, not as whole quanta. Measured: a PUBLIC KEY body of `A` ahead of our CA gave guard=accept while node reported `bad base64 decode` and loaded zero extra CAs. Padding is positional too — `AAA=` and `AA==` load, `A===`, `=AAA` and `AA=A` do not. Now length%4==0 plus trailing-only padding: 16/16 agreement with a real handshake on the body shapes measured. - The label pattern was [A-Z0-9 ], so every other legal PEM label was invisible while openssl still treated the block as real. Measured: a malformed `X-FOO` block gave guard=accept, node loaded zero CAs. Every label tried behaved as a real block (hyphenated, lowercase, underscored, dotted, punctuated, empty), so the label now decides only WHICH check a block gets, never whether it is one. Note `[^-]*` does NOT fix this — `-` is legal inside a label, so the stop condition is the `-----` run. - The banner suppression keyed on `process.channel`, which only proves SOME parent opened an IPC descriptor. Measured: a plain fork() of server.mjs (which this suite itself does, and any supervisor may) got the suppressed banner plus the false claim that a launcher had wired the client — leaving an operator with no wiring instructions at all. Now an explicit CACHE_FIX_WIRED_BY_LAUNCHER the launcher sets. This is an internal handshake between the two files, not an operator knob, and is deliberately undocumented as one. Also fixes the test-suite temp-dir leak reported in the first review and skipped then. Measured: one run of proxy-wrapper.test.mjs left 38 dirs behind, and a /tmp that had accumulated 1954 of them held 432 ca.key / leaf.key files — forward mode mints an RSA CA and leaf per config dir, so the leak is private key material, not empty directories. Registered centrally with one after() hook rather than per-test rmSync, because a failing test throws before its own cleanup and every future test would have to remember. A leak is invisible to assertions (measured: suite still reported 23 pass / 0 fail while leaking 39 dirs), so the guard is a source-level check that nothing bypasses the registrar. Coverage: 164 measured shapes across four sweeps, 0 false accepts. Each of the three fixes plus the registrar is mutation-verified — reverting it fails exactly one test. Full suite 1504 pass / 2 fail, both EMFILE from an fs.watch test that fails identically at the merge base. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d2828c2 commit 2c0ea06

4 files changed

Lines changed: 131 additions & 30 deletions

File tree

bin/ca-trust.mjs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,25 @@ import { X509Certificate } from "node:crypto";
33
// Does a non-certificate block's armor decode? Node only needs that much from a
44
// CRL or key block, so this is deliberately weaker than parsing it as whatever
55
// it claims to be — the guard's job is to predict node's loader, not to
6-
// validate the block's contents. A body with a `-` in it reads as decodable
7-
// here and node agrees, but openssl stops at the dash, so we call it damaged;
8-
// that is the conservative direction and the only measured disagreement.
6+
// validate the block's contents.
7+
//
8+
// Base64 is checked as whole 4-character quanta, not merely as an alphabet. An
9+
// alphabet-only test accepted a one-character body: measured, `A` in a
10+
// PUBLIC KEY block ahead of our CA gave guard=accept while node reported
11+
// `bad base64 decode` and loaded zero extra CAs. Padding is equally positional —
12+
// `AAA=` and `AA==` load, `A===`, `=AAA` and `AA=A` do not. Measured 16/16
13+
// agreement with a real handshake on the rule below.
14+
//
15+
// A body containing `-` reads as damaged here even though node accepts it
16+
// (openssl stops at the dash), which is the conservative direction and the only
17+
// measured disagreement.
918
function isBase64Body(block, endMarker) {
1019
const bodyStart = block.indexOf("\n") + 1;
1120
const bodyEnd = block.lastIndexOf(endMarker);
1221
if (bodyStart === 0 || bodyEnd < bodyStart) return false;
1322
const body = block.slice(bodyStart, bodyEnd).replace(/\s+/g, "");
14-
return body.length > 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(body);
23+
if (body.length === 0 || body.length % 4 !== 0) return false;
24+
return /^[A-Za-z0-9+/]+={0,2}$/.test(body);
1525
}
1626

1727
// Is this merged CA bundle safe to hand claude as NODE_EXTRA_CA_CERTS?
@@ -49,7 +59,17 @@ export function bundleCarriesOurCA(text, ourCaPem) {
4959
// pattern made that block invisible to us while node still tried to load it
5060
// — measured: a corrupt block wearing a trailing space was skipped by the
5161
// guard and failed the handshake.
52-
const marker = /^-----BEGIN ([A-Z0-9 ]*)-----[ \t]*$/gm;
62+
// The label pattern is permissive on purpose. Restricting it to uppercase,
63+
// digits and spaces made every other legal label invisible to us while
64+
// openssl still treated the block as real — measured: a malformed `X-FOO`
65+
// block ahead of our CA gave guard=accept while node loaded zero extra CAs.
66+
// Every label tried behaved as a real block (hyphenated, lowercase,
67+
// underscored, dotted, punctuated, even empty), so the label decides only
68+
// WHICH check a block gets, never whether it is one.
69+
// `-` is legal INSIDE a label, so the stop condition is the `-----` run, not
70+
// the first hyphen: `[^-]*` failed to match `X-FOO` at all, which is the same
71+
// blind spot in a new place.
72+
const marker = /^-----BEGIN ((?:(?!-----).)*)-----[ \t]*$/gm;
5373
let carriesUs = false;
5474
for (let m; (m = marker.exec(text)); ) {
5575
const label = m[1];

bin/claude-via-proxy.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ if (proxyUpstream) proxyEnv.CACHE_FIX_PROXY_UPSTREAM = proxyUpstream;
134134
// or the HTTPS_PROXY wiring below would tunnel to a proxy that only speaks
135135
// reverse-proxy and never terminates TLS for the upstream host.
136136
if (remoteControl) proxyEnv.CACHE_FIX_FORWARD_PROXY = "on";
137+
// Tell the proxy we will wire claude ourselves, so it does not print a recipe
138+
// that would undo that. Internal handshake between these two files, deliberately
139+
// not documented as an operator knob — see the banner in proxy/server.mjs.
140+
if (remoteControl) proxyEnv.CACHE_FIX_WIRED_BY_LAUNCHER = "1";
137141

138142
const proxyProc = fork(SERVER_PATH, [], {
139143
stdio: ["ignore", "pipe", "pipe", "ipc"],

proxy/server.mjs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -614,12 +614,20 @@ export async function startProxy(options = {}) {
614614

615615
const addr = server.address();
616616
if (forwardProxyCA) {
617-
const wiredByLauncher = process.channel !== undefined;
618-
// Only when the operator is the one wiring. process.channel is set exactly
619-
// when our launcher fork()ed us, and it has already wired claude via
620-
// ca-trust.d — printing `export NODE_EXTRA_CA_CERTS=<our ca.pem>` into its
621-
// relayed stderr tells the operator to undo that: the variable takes one
622-
// file, so pinning it to our CA alone untrusts every other MITM on the host.
617+
// Only when the operator is the one wiring. Under --remote-control the
618+
// launcher has already wired claude via ca-trust.d and relays this stderr,
619+
// so printing `export NODE_EXTRA_CA_CERTS=<our ca.pem>` there tells the
620+
// operator to undo it: the variable takes one file, so pinning it to our CA
621+
// alone untrusts every other MITM on the host.
622+
//
623+
// Keyed on a signal the launcher sets explicitly, NOT on `process.channel`.
624+
// A channel only proves some parent opened an IPC descriptor — measured, a
625+
// plain `fork()` of this file (which the test suite does, and any supervisor
626+
// may) got the suppressed banner plus the false claim that a launcher had
627+
// wired the client, leaving an operator with no wiring instructions at all.
628+
// This is an internal handshake, not an operator knob: it asserts "my parent
629+
// already wired me", which nothing but the launcher can truthfully say.
630+
const wiredByLauncher = process.env.CACHE_FIX_WIRED_BY_LAUNCHER === "1";
623631
process.stderr.write(wiredByLauncher
624632
? "[cache-fix] forward-proxy: on. Client wired by the launcher (ca-trust.d).\n"
625633
: "[cache-fix] forward-proxy: on. Wire the client (leave ANTHROPIC_BASE_URL UNSET so Remote Control stays enabled):\n" +

test/proxy-wrapper.test.mjs

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,37 @@
1-
import { describe, it } from "node:test";
1+
import { after, describe, it } from "node:test";
22
import assert from "node:assert/strict";
33
import { fork } from "node:child_process";
44
import { fileURLToPath } from "node:url";
55
import { dirname, resolve, join } from "node:path";
66
import { tmpdir } from "node:os";
7-
import { closeSync, existsSync, fstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, statSync, utimesSync, writeFileSync } from "node:fs";
7+
import { closeSync, existsSync, fstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
88
import http from "node:http";
99

1010
const __dirname = dirname(fileURLToPath(import.meta.url));
1111
const WRAPPER_PATH = resolve(__dirname, "../bin/claude-via-proxy.mjs");
1212
const SERVER_PATH = resolve(__dirname, "../proxy/server.mjs");
1313

14+
// Every temp dir this file makes, removed once at the end.
15+
//
16+
// Registered centrally rather than rmSync'd per test: forward mode mints an RSA
17+
// CA and leaf inside each config dir, so a leak is not an empty directory, it is
18+
// private key material. Measured before this: one `node --test` of this file
19+
// left 38 dirs behind, and a /tmp that had accumulated 1954 of them held 432
20+
// ca.key / leaf.key files. Per-test cleanup would also skip exactly the runs
21+
// that matter — a failing test throws before its own rmSync — and every future
22+
// test would have to remember. after() runs on pass and on fail.
23+
const tempDirs = [];
24+
function tempDir(prefix) {
25+
const d = mkdtempSync(join(tmpdir(), prefix));
26+
tempDirs.push(d);
27+
return d;
28+
}
29+
after(() => {
30+
for (const d of tempDirs) {
31+
try { rmSync(d, { recursive: true, force: true }); } catch { /* already gone */ }
32+
}
33+
});
34+
1435
describe("proxy server lifecycle", () => {
1536
it("starts and responds to health check", async () => {
1637
const proxyProc = fork(SERVER_PATH, [], {
@@ -78,7 +99,7 @@ function cleanEnv(overrides) {
7899
// bundle advertising a CA nothing signs with — precisely the failure this
79100
// feature exists to prevent. It also silently poisons the suite itself: two
80101
// cases were reading the host's real merged bundle instead of a fixture.
81-
env.CLAUDE_CONFIG_DIR = mkdtempSync(join(tmpdir(), "cffcfg-"));
102+
env.CLAUDE_CONFIG_DIR = tempDir("cffcfg-");
82103
return { ...env, ...overrides };
83104
}
84105

@@ -171,7 +192,7 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
171192
// developer's real ~/.claude/ca-trust.pem. On a machine where a bundle builder
172193
// has run, that file exists and legitimately wins — the assertion would fail
173194
// for a host-state reason, not a code reason.
174-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
195+
const configDir = tempDir("cfftrust-");
175196
const wrapperProc = fork(WRAPPER_PATH, ["--remote-control", "--proxy-port", "0"], {
176197
stdio: ["ignore", "pipe", "pipe", "ipc"],
177198
env: cleanEnv({ CACHE_FIX_CLAUDE_CMD: `${NODE} -e ${script}`, CLAUDE_CONFIG_DIR: configDir }),
@@ -199,7 +220,7 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
199220
// same order, or it points claude at a different (or absent) CA than the one
200221
// the spawned proxy generated — a hard fail, or a silent trust mismatch when
201222
// a stale default CA exists. This test pins the override path exactly.
202-
const caDir = mkdtempSync(join(tmpdir(), "cffcadir-"));
223+
const caDir = tempDir("cffcadir-");
203224
const script =
204225
'process.stdout.write("BASE="+(process.env.ANTHROPIC_BASE_URL||"UNSET")+' +
205226
'"|CA="+(process.env.NODE_EXTRA_CA_CERTS||"UNSET")+"\\n")';
@@ -240,7 +261,7 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
240261
// Publishing is how OTHER components learn to trust us, and it must happen
241262
// before the client runs — a bundle builder that reads the dir on a cold
242263
// start would otherwise miss us and produce a bundle without our CA.
243-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
264+
const configDir = tempDir("cfftrust-");
244265
const script = 'process.stdout.write("OK\\n")';
245266
const wrapperProc = fork(WRAPPER_PATH, ["--remote-control", "--proxy-port", "0"], {
246267
stdio: ["ignore", "pipe", "pipe", "ipc"],
@@ -276,7 +297,7 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
276297
// fixture without it is the stale-bundle case, which is correctly rejected
277298
// (see the test below). So: run once to let the proxy generate + publish our
278299
// CA, then build the bundle from it the way the launcher would, then re-run.
279-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
300+
const configDir = tempDir("cfftrust-");
280301
const bundle = join(configDir, "ca-trust.pem");
281302
const script = 'process.stdout.write("CA="+(process.env.NODE_EXTRA_CA_CERTS||"UNSET")+"\\n")';
282303
const runOnce = () => runWrapper(script, { CLAUDE_CONFIG_DIR: configDir });
@@ -295,7 +316,7 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
295316
// Single-writer invariant. Two launchers both "helpfully" rebuilding the
296317
// merged file race one output, and a component that rewrites a sibling's pem
297318
// can untrust it. So: we write exactly one path, ca-trust.d/ccf.pem.
298-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
319+
const configDir = tempDir("cfftrust-");
299320
const trustDir = join(configDir, "ca-trust.d");
300321
mkdirSync(trustDir, { recursive: true });
301322
const sibling = join(trustDir, "other-component.pem");
@@ -324,7 +345,7 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
324345
it("--remote-control falls back to its own CA when no merged bundle exists (unchanged standalone behaviour)", async () => {
325346
// A plain CCF user with no other MITM and no bundle builder must see exactly
326347
// what they saw before this contract existed.
327-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
348+
const configDir = tempDir("cfftrust-");
328349
const script = 'process.stdout.write("CA="+(process.env.NODE_EXTRA_CA_CERTS||"UNSET")+"\\n")';
329350
const wrapperProc = fork(WRAPPER_PATH, ["--remote-control", "--proxy-port", "0"], {
330351
stdio: ["ignore", "pipe", "pipe", "ipc"],
@@ -377,7 +398,7 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
377398
// the very bytes that reader is consuming or leaves it on a deleted inode
378399
// whose content is gone. So: hold a descriptor open across the launch, then
379400
// read it to the end.
380-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
401+
const configDir = tempDir("cfftrust-");
381402
const trustDir = join(configDir, "ca-trust.d");
382403
const dst = join(trustDir, "ccf.pem");
383404
mkdirSync(trustDir, { recursive: true });
@@ -480,7 +501,7 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
480501
// Both fixtures exist in the same directory across one launch, so the test
481502
// fails if the reaper is unconditional (fresh one dies) OR absent (old one
482503
// survives) — one launch, two opposite outcomes.
483-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
504+
const configDir = tempDir("cfftrust-");
484505
const trustDir = join(configDir, "ca-trust.d");
485506
mkdirSync(trustDir, { recursive: true });
486507
const stale = join(trustDir, "ccf.pem.99999.aaaaaaaa-orphan");
@@ -511,7 +532,7 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
511532
// portably: rename() onto it fails EISDIR, and unlike a permission fixture
512533
// it behaves the same when the suite runs as root (measured: chmod-based
513534
// fixtures pass vacuously in a root container, which is how CI runs).
514-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
535+
const configDir = tempDir("cfftrust-");
515536
const trustDir = join(configDir, "ca-trust.d");
516537
mkdirSync(join(trustDir, "ccf.pem"), { recursive: true });
517538
const stale = join(trustDir, "ccf.pem.99999.aaaaaaaa-orphan");
@@ -534,7 +555,7 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
534555
// bundle. An operator following the line on screen pins the variable to our
535556
// CA alone for every later process, silently untrusting every other MITM —
536557
// the exact failure the contract exists to prevent.
537-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
558+
const configDir = tempDir("cfftrust-");
538559
const { code, err } = await runWrapper('process.stdout.write("OK\\n")', { CLAUDE_CONFIG_DIR: configDir });
539560
assert.equal(code, 0, `Expected exit 0, got ${code}. stderr: ${err}`);
540561
assert.doesNotMatch(err, /export NODE_EXTRA_CA_CERTS=/,
@@ -553,8 +574,8 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
553574
// failed to parse. The session then failed every request with
554575
// UNABLE_TO_VERIFY_LEAF_SIGNATURE while the only diagnostic pointed at the
555576
// wrong component.
556-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
557-
const caDir = mkdtempSync(join(tmpdir(), "cffca-"));
577+
const configDir = tempDir("cfftrust-");
578+
const caDir = tempDir("cffca-");
558579
// ca.key must be present alongside it: the proxy's reuse guard keys on
559580
// existsSync(ca.pem) && existsSync(ca.key), so a corrupt ca.pem with its key
560581
// still beside it is REUSED rather than regenerated. That is what makes this
@@ -583,8 +604,8 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
583604
// proven unreadable — the message was right and the behavior was unchanged.
584605
// Unset is the honest state: we have no usable CA to add, so node falls back
585606
// to its built-in store rather than to a file we vouch for and cannot read.
586-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
587-
const caDir = mkdtempSync(join(tmpdir(), "cffca-"));
607+
const configDir = tempDir("cfftrust-");
608+
const caDir = tempDir("cffca-");
588609
// ca.key beside it, or the proxy regenerates and the state is unreachable.
589610
writeFileSync(join(caDir, "ca.key"), "-----BEGIN PRIVATE KEY-----\nplaceholder\n-----END PRIVATE KEY-----\n");
590611
writeFileSync(join(caDir, "ca.pem"), "-----BEGIN CERTIFICATE-----\ntruncated\n");
@@ -605,7 +626,7 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
605626
// normal state right after a CCF upgrade, so this is not a corner case.
606627
// (a sibling component hit the same hazard from the other side and guards
607628
// it identically.)
608-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
629+
const configDir = tempDir("cfftrust-");
609630
const bundle = join(configDir, "ca-trust.pem");
610631
// A plausible stale bundle: real PEM content, just not ours.
611632
writeFileSync(bundle, "-----BEGIN CERTIFICATE-----\nc3RhbGUtYnVuZGxlLXdpdGhvdXQtb3VyLUNB\n-----END CERTIFICATE-----\n");
@@ -650,7 +671,7 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
650671
// and containment together still do not prove Node verifies with the result —
651672
// only a handshake does. They are here to keep a KNOWN-bad bundle from ever
652673
// reaching the client.
653-
const configDir = mkdtempSync(join(tmpdir(), "cfftrust-"));
674+
const configDir = tempDir("cfftrust-");
654675
const bundle = join(configDir, "ca-trust.pem");
655676
const script = 'process.stdout.write("CA="+(process.env.NODE_EXTRA_CA_CERTS||"UNSET")+"\\n")';
656677
const runOnce = () => runWrapper(script, { CLAUDE_CONFIG_DIR: configDir });
@@ -783,4 +804,52 @@ describe("launch wrapper (claude-via-proxy)", { concurrency: 1 }, () => {
783804
assert.equal(occurrences, 1, `127.0.0.1 should appear exactly once, got NP=${np}`);
784805
assert.ok(np.split(",").includes("localhost"), `localhost should be added, got NP=${np}`);
785806
});
807+
808+
it("a plain fork of the server still gets the wiring recipe", async () => {
809+
// The suppression must key on the launcher, not on "someone fork()ed me".
810+
// Keying it on process.channel was measured suppressing the recipe for THIS
811+
// suite's own forks and for any supervisor's — the operator got no wiring
812+
// instructions plus a false claim that a launcher had wired the client.
813+
const caDir = tempDir("cffca-");
814+
const p = fork(SERVER_PATH, [], {
815+
stdio: ["ignore", "pipe", "pipe", "ipc"],
816+
env: cleanEnv({ CACHE_FIX_FORWARD_PROXY: "on", CACHE_FIX_CA_DIR: caDir, CACHE_FIX_PROXY_PORT: "0" }),
817+
});
818+
let err = "";
819+
p.stderr.on("data", (c) => { err += c.toString(); });
820+
await new Promise((res) => setTimeout(res, 6000));
821+
p.kill("SIGTERM");
822+
await new Promise((res) => p.on("exit", res));
823+
824+
assert.match(err, /export NODE_EXTRA_CA_CERTS=/,
825+
`a non-launcher fork must still be told how to wire; stderr: ${err}`);
826+
assert.doesNotMatch(err, /Client wired by the launcher/,
827+
`nothing wired this client, so it must not claim otherwise; stderr: ${err}`);
828+
});
829+
830+
it("creates every temp dir through the registrar, so none outlive the run", () => {
831+
// A leak is invisible to every other assertion in this file — measured: with
832+
// the cleanup neutered the suite still reported 23 pass / 0 fail while
833+
// leaving 39 directories behind. So the thing to pin is not "the dirs are
834+
// gone" (after() has not run yet when a test executes) but "nothing bypasses
835+
// the registrar", which is the only way one can survive.
836+
//
837+
// Source-level on purpose: forward mode mints an RSA CA and leaf inside each
838+
// config dir, so a bypassed site leaks private key material, and the next
839+
// person to add a test is exactly who would reintroduce it.
840+
// Matched on the call shape taking a string literal, which the registrar
841+
// itself does not have (it takes `prefix`). Comment lines are skipped and
842+
// the pattern is assembled rather than written out, so neither this
843+
// assertion nor the prose above it can flag itself.
844+
const src = readFileSync(new URL(import.meta.url), "utf8");
845+
const call = new RegExp(["mkdtempSync\\(join\\(tmpdir\\(\\),", "\\s*\"[^\"]+\"\\s*\\)\\)"].join(""));
846+
const raw = src.split("\n")
847+
.map((line, i) => [i + 1, line])
848+
.filter(([, line]) => !line.trim().startsWith("//"))
849+
.filter(([, line]) => call.test(line))
850+
.map(([n]) => n);
851+
assert.deepEqual(raw, [],
852+
`every temp dir must go through tempDir(); raw mkdtempSync at line(s): ${raw.join(", ")}`);
853+
assert.ok(tempDirs.length > 0, "premise: this file does create temp dirs");
854+
});
786855
});

0 commit comments

Comments
 (0)