@junghanacs/entwurf 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/BASELINE.md +23 -5
- package/CHANGELOG.md +236 -0
- package/DELIVERY.md +59 -6
- package/README.md +40 -9
- package/VERIFY.md +9 -1
- package/docs/acp-backend-rail.md +9 -1
- package/docs/external-mcp-host.md +6 -3
- package/docs/setup-clean-host.md +93 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +6 -2
- package/package.json +2 -2
- package/pi-extensions/acp-provider.ts +1 -1
- package/pi-extensions/lib/acp/models.ts +3 -3
- package/pi-extensions/lib/acp/overlay.ts +6 -2
- package/pi-extensions/lib/meta-session.ts +25 -0
- package/run.sh +71 -26
- package/scripts/agy-bridge.sh +1 -1
- package/scripts/agy-imprint.sh +10 -7
- package/scripts/check-acp-carrier-augment.ts +6 -5
- package/scripts/check-acp-cortex.ts +41 -0
- package/scripts/check-acp-provider-surface.ts +9 -6
- package/scripts/check-copilot-birth-hook.ts +3 -1
- package/scripts/check-copilot-receive-arm.ts +169 -21
- package/scripts/check-copilot-statusline.ts +3 -1
- package/scripts/check-gate-qualification.ts +10 -9
- package/scripts/check-hook-launch-topology.ts +6 -5
- package/scripts/check-install-surface.ts +2 -1
- package/scripts/check-meta-facts.ts +6 -4
- package/scripts/check-meta-hook-session-switch.ts +4 -3
- package/scripts/check-meta-identity-consumers.ts +8 -7
- package/scripts/check-meta-receiver-marker.ts +2 -1
- package/scripts/check-omp-birth-hook.ts +3 -2
- package/scripts/check-probe-bridge-command.ts +3 -1
- package/scripts/check-setup-qualification.sh +21 -0
- package/scripts/copilot-bridge-doctor.sh +15 -5
- package/scripts/copilot-bridge-install.sh +6 -2
- package/scripts/copilot-launch.sh +16 -2
- package/scripts/copilot-receive-bridge.sh +80 -13
- package/scripts/lib/reclaim-on-exit.ts +86 -0
- package/scripts/meta-bridge-doctor.sh +56 -15
- package/scripts/meta-bridge-install.sh +13 -7
- package/scripts/meta-bridge-uninstall.sh +6 -4
- package/scripts/mutants/acp-cortex.json +26 -2
- package/scripts/mutants/copilot-receive.json +26 -0
- package/scripts/mutants/omp-birth.json +23 -6
- package/scripts/mutants/setup-verdict.json +13 -0
- package/scripts/omp-bridge-doctor.sh +110 -41
- package/scripts/omp-bridge-install.sh +6 -2
- package/scripts/omp-receive-install.sh +7 -2
- package/scripts/raw-async-delivery/README.md +10 -1
- package/scripts/raw-codex-measure/README.md +689 -0
- package/scripts/raw-codex-measure/source-audit.md +243 -0
- package/scripts/raw-macos-measure/README.md +148 -0
- package/scripts/raw-macos-measure/probe.sh +389 -0
- package/scripts/smoke-meta-async-drift.sh +7 -5
- package/scripts/smoke-meta-install-state.sh +70 -19
- package/scripts/smoke-omp-bridge-state.sh +38 -0
- package/scripts/smoke-setup-verdict.sh +100 -0
|
@@ -323,6 +323,47 @@ const enrichedServers: AcpMcpServer[] = [
|
|
|
323
323
|
/absolute realHome/,
|
|
324
324
|
"a non-absolute realHome must refuse (the child must never re-derive it)",
|
|
325
325
|
);
|
|
326
|
+
assert.throws(
|
|
327
|
+
() =>
|
|
328
|
+
ensureCortexDualHomeOverlay({
|
|
329
|
+
scopeKey: "pi:sess-empty",
|
|
330
|
+
mcpServers: [],
|
|
331
|
+
realHome: "",
|
|
332
|
+
overlaysRoot,
|
|
333
|
+
isPidAlive: () => true,
|
|
334
|
+
}),
|
|
335
|
+
/absolute realHome/,
|
|
336
|
+
"an empty realHome must refuse — the contract sentence is 'absolute', and '' is not",
|
|
337
|
+
);
|
|
338
|
+
// …and the guard states PATH-FLAVOR absoluteness, not the host it runs on:
|
|
339
|
+
// a drive/UNC realHome is absolute in the win32 flavor, so a POSIX host must
|
|
340
|
+
// NOT read it as relative. (This asserts nothing about native-Windows support
|
|
341
|
+
// — only that the refusal rule is decided by the path, not by process.platform.)
|
|
342
|
+
// realSnowflakeHome is pinned under tmp so the positive cells exercise the
|
|
343
|
+
// GUARD alone: the win32-flavored realHome never reaches a POSIX join for fs
|
|
344
|
+
// work, and every path the overlay materializes stays under overlaysRoot.
|
|
345
|
+
for (const winFlavored of ["C:\\Users\\x", "\\\\server\\share"]) {
|
|
346
|
+
let winOverlay: ReturnType<typeof ensureCortexDualHomeOverlay> | undefined;
|
|
347
|
+
assert.doesNotThrow(
|
|
348
|
+
() => {
|
|
349
|
+
winOverlay = ensureCortexDualHomeOverlay({
|
|
350
|
+
scopeKey: `pi:sess-win-${winFlavored.length}`,
|
|
351
|
+
mcpServers: [],
|
|
352
|
+
realHome: winFlavored,
|
|
353
|
+
realSnowflakeHome: realSnowflake,
|
|
354
|
+
overlaysRoot,
|
|
355
|
+
isPidAlive: () => true,
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
`a win32-flavored absolute realHome (${JSON.stringify(winFlavored)}) must PASS the guard — absoluteness is judged by PATH FLAVOR, never by the host the gate runs on [QK:CORTEX-REALHOME-PLATFORM-NEUTRAL]`,
|
|
359
|
+
);
|
|
360
|
+
assert.ok(
|
|
361
|
+
winOverlay !== undefined &&
|
|
362
|
+
winOverlay.scopeDir.startsWith(overlaysRoot) &&
|
|
363
|
+
existsSync(join(winOverlay.snowflakeHome, "cortex")),
|
|
364
|
+
"the accepted win32-flavored realHome must still materialize entirely under the overlays root (the guard is the only thing the flavor cell widens)",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
326
367
|
} finally {
|
|
327
368
|
rmSync(root, { recursive: true, force: true });
|
|
328
369
|
}
|
|
@@ -138,19 +138,22 @@ try {
|
|
|
138
138
|
// Set comparison (sorted, exact) — not `includes`: a substring/anchor probe
|
|
139
139
|
// cannot see a row that VANISHED, and the cortex ids contain the claude ids as
|
|
140
140
|
// substrings. Expected is derived from the two source id constants so there is
|
|
141
|
-
// one SSOT; the literal
|
|
142
|
-
// curation list
|
|
143
|
-
// together
|
|
141
|
+
// one SSOT; the literal 7 below is the independent floor that catches a
|
|
142
|
+
// curation list that SHRANK — expected derives from the same constants, so a
|
|
143
|
+
// dropped row moves expected and captured together and the set comparison
|
|
144
|
+
// stays green. That is why the count is asserted separately and carries its
|
|
145
|
+
// own claim: the cortex list has `CORTEX-CURATED-FOUR-ROWS` for exactly this
|
|
146
|
+
// failure, and the claude list needs its own oracle rather than borrowing it.
|
|
144
147
|
const expectedIds = [...SUPPORTED_ANTHROPIC_MODEL_IDS, ...SUPPORTED_CORTEX_MODEL_IDS].slice().sort();
|
|
145
148
|
assert.equal(
|
|
146
149
|
expectedIds.length,
|
|
147
|
-
|
|
148
|
-
`curated id constants must total
|
|
150
|
+
7,
|
|
151
|
+
`[QK:CLAUDE-CURATED-THREE-ROWS] curated id constants must total 7 rows (claude 3 + cortex 4) — got ${expectedIds.length}: ${expectedIds.join(", ")}`,
|
|
149
152
|
);
|
|
150
153
|
assert.deepEqual(
|
|
151
154
|
capIds.slice().sort(),
|
|
152
155
|
expectedIds,
|
|
153
|
-
`[QK:CORTEX-PROVIDER-
|
|
156
|
+
`[QK:CORTEX-PROVIDER-EXACT-ROW-SURFACE] compiled entry must register the EXACT curated set of both adapters (claude 3 + cortex 4) — got: ${capIds.join(", ") || "none"}`,
|
|
154
157
|
);
|
|
155
158
|
assert.equal(typeof cap.cfg.streamSimple, "function", "entry streamSimple must be a function");
|
|
156
159
|
// Regression guard: the entry must wire the REAL backend (streamShellAcp),
|
|
@@ -52,6 +52,7 @@ import { resolveFactList } from "../pi-extensions/lib/entwurf-facts.ts";
|
|
|
52
52
|
import { nativePushSupported } from "../pi-extensions/lib/entwurf-v2-contract.ts";
|
|
53
53
|
import { META_SENDER_BACKENDS, resolveTrustedMetaSenderIdentity } from "../pi-extensions/lib/meta-sender-identity.ts";
|
|
54
54
|
import { listAllMetaIdentitiesDir, processStartKey } from "../pi-extensions/lib/meta-session.ts";
|
|
55
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
55
56
|
|
|
56
57
|
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
57
58
|
const PLUGIN = "entwurf-meta-receive-copilot";
|
|
@@ -63,7 +64,8 @@ function ok(label: string, cond: boolean): void {
|
|
|
63
64
|
passed++;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
const root = mkdtempSync(path.join(tmpdir(), "entwurf-copilot-birth."));
|
|
67
|
+
const root = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "entwurf-copilot-birth.")));
|
|
68
|
+
|
|
67
69
|
const asm = path.join(root, "asm");
|
|
68
70
|
|
|
69
71
|
// ── 1. the real assembler ────────────────────────────────────────────────────
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
upsertMetaSession,
|
|
61
61
|
writeMetaSenderMarker,
|
|
62
62
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
63
|
+
import { killOnExit, reclaimNow, reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
63
64
|
|
|
64
65
|
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
65
66
|
const UNIT = "entwurf-receive";
|
|
@@ -90,7 +91,7 @@ async function waitUntil(fn: () => boolean, ms = 15000): Promise<boolean> {
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
const root = mkdtempSync(path.join(tmpdir(), "entwurf-copilot-receive."));
|
|
94
|
+
const root = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "entwurf-copilot-receive.")));
|
|
94
95
|
const home = path.join(root, "home");
|
|
95
96
|
const store = path.join(root, "agent");
|
|
96
97
|
const extRoot = path.join(root, "extensions");
|
|
@@ -260,6 +261,30 @@ writeFileSync(
|
|
|
260
261
|
path.join(stubDir, "register.mjs"),
|
|
261
262
|
`import { register } from "node:module";
|
|
262
263
|
register("./loader.mjs", import.meta.url);
|
|
264
|
+
// The vendor half this fixture MUST model. Production children are started by the CLI's
|
|
265
|
+
// own bootstrap (\`preloads/extension_bootstrap.mjs\`), which exits the extension when its
|
|
266
|
+
// parent disappears — checked at startup and then once a second — and the receiver's
|
|
267
|
+
// owner-pid contract is built on that. A stub that omitted it made the fixture MORE
|
|
268
|
+
// permissive than production: the stub's referenced timer kept every child alive after
|
|
269
|
+
// the gate died, so each red run reparented its children to pid 1 forever (measured
|
|
270
|
+
// 2026-09-07/08 on \`oracle\`: 360 such node processes, 720 temp roots). The check is
|
|
271
|
+
// \`unref\`ed so it never becomes the reason a child stays up.
|
|
272
|
+
//
|
|
273
|
+
// KNOWN-OPEN, deliberately: under a subreaper (\`PR_SET_CHILD_SUBREAPER\`, systemd-run)
|
|
274
|
+
// a parent that dies before this module's FIRST tick is already reparented to a LIVE
|
|
275
|
+
// subreaper, so \`parentAtStart\` records that pid and never changes again. The obvious
|
|
276
|
+
// patch — probing the recorded pid with signal 0 — closes nothing: Linux reparents in
|
|
277
|
+
// \`forget_original_parent()\` BEFORE the old pid becomes unreachable, and a zombie
|
|
278
|
+
// parent still answers signal 0, so the probe is false at startup in exactly the case
|
|
279
|
+
// it was written for. The honest close is to watch the DECLARED parent
|
|
280
|
+
// (\`COPILOT_EXTENSION_PARENT_PID\`), which this gate deliberately sets to a pid that is
|
|
281
|
+
// not the child's real parent, so it is left documented rather than half-fixed. The
|
|
282
|
+
// window is microseconds and no measured orphan on this host came through it.
|
|
283
|
+
const parentAtStart = process.ppid;
|
|
284
|
+
if (parentAtStart === 1) process.exit(0);
|
|
285
|
+
setInterval(() => {
|
|
286
|
+
if (process.ppid !== parentAtStart) process.exit(0);
|
|
287
|
+
}, 1000).unref();
|
|
263
288
|
`,
|
|
264
289
|
);
|
|
265
290
|
|
|
@@ -274,23 +299,25 @@ interface Harness {
|
|
|
274
299
|
function launch(label: string, sessionId: string, extra: NodeJS.ProcessEnv = {}): Harness {
|
|
275
300
|
const box = path.join(root, `box-${label}`);
|
|
276
301
|
mkdirSync(box, { recursive: true });
|
|
277
|
-
const child =
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
302
|
+
const child = killOnExit(
|
|
303
|
+
spawn(process.execPath, ["--import", path.join(stubDir, "register.mjs"), entry], {
|
|
304
|
+
env: {
|
|
305
|
+
...process.env,
|
|
306
|
+
HOME: home,
|
|
307
|
+
PI_CODING_AGENT_DIR: store,
|
|
308
|
+
STUB_BOX: box,
|
|
309
|
+
STUB_SDK: path.join(stubDir, "extension.js"),
|
|
310
|
+
STUB_SESSION_ID: sessionId,
|
|
311
|
+
SESSION_ID: sessionId,
|
|
312
|
+
// The vendor's carrier. This gate process IS the fork parent, which is also
|
|
313
|
+
// what makes the sender-marker join real rather than mocked: the marker below
|
|
314
|
+
// is written under the very pid the child will look itself up by.
|
|
315
|
+
COPILOT_EXTENSION_PARENT_PID: String(process.pid),
|
|
316
|
+
...extra,
|
|
317
|
+
},
|
|
318
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
294
321
|
child.stdout?.resume();
|
|
295
322
|
child.stderr?.resume();
|
|
296
323
|
return {
|
|
@@ -699,6 +726,68 @@ ok(
|
|
|
699
726
|
opaque.kill("SIGKILL");
|
|
700
727
|
}
|
|
701
728
|
}
|
|
729
|
+
{
|
|
730
|
+
// AN UNREADABLE argv IS "UNKNOWN", NOT "ABSENT" (#78 B2). Identity is the FIRST
|
|
731
|
+
// predicate of the scan, so a candidate whose `cmdline` will not open cannot be
|
|
732
|
+
// classified at all — and a blanket `except OSError: return None` classified it
|
|
733
|
+
// anyway, as "not a Copilot CLI". Every candidate taking that branch left all
|
|
734
|
+
// counters at zero, which is the benign "no live GitHub Copilot CLI process" note:
|
|
735
|
+
// a doctor claiming ABSENCE while its identity input was unavailable. That is the
|
|
736
|
+
// same false-success class as the platform branch above, so it gets the same verdict.
|
|
737
|
+
//
|
|
738
|
+
// THE DENIAL IS INJECTED, AND THAT IS SAID OUT LOUD BECAUSE IT MATTERS. The environ
|
|
739
|
+
// cell above makes its file really unreadable with prctl(PR_SET_DUMPABLE, 0), and
|
|
740
|
+
// measured on this kernel that trick does NOT reach argv: `/proc/<pid>/cmdline` stays
|
|
741
|
+
// mode 0444 and readable even for a non-dumpable child (a full /proc census read
|
|
742
|
+
// 340/340 cmdlines). A real EACCES there needs a `hidepid` /proc mount, which an
|
|
743
|
+
// unprivileged gate cannot create. So the refusal is raised exactly where the kernel
|
|
744
|
+
// would raise it — at the scanner's own read, with EACCES — through a `sitecustomize`
|
|
745
|
+
// on PYTHONPATH, and what this cell pins is the CLASSIFICATION of a refused read,
|
|
746
|
+
// which is the entire contract. The fixture process is real, and the control run
|
|
747
|
+
// asserts it WOULD be identified as a native CLI when its argv can be read, so
|
|
748
|
+
// neither half of the pin is vacuous.
|
|
749
|
+
const denyDir = path.join(root, "deny-cmdline");
|
|
750
|
+
mkdirSync(denyDir, { recursive: true });
|
|
751
|
+
writeFileSync(
|
|
752
|
+
path.join(denyDir, "sitecustomize.py"),
|
|
753
|
+
[
|
|
754
|
+
"import errno, os, pathlib",
|
|
755
|
+
"_pid = os.environ.get('ENTWURF_TEST_DENY_CMDLINE_PID')",
|
|
756
|
+
"if _pid:",
|
|
757
|
+
" _target = '/proc/' + _pid + '/cmdline'",
|
|
758
|
+
" _real = pathlib.Path.read_bytes",
|
|
759
|
+
" def _denied(self):",
|
|
760
|
+
" if str(self) == _target:",
|
|
761
|
+
" raise PermissionError(errno.EACCES, 'Permission denied', str(self))",
|
|
762
|
+
" return _real(self)",
|
|
763
|
+
" pathlib.Path.read_bytes = _denied",
|
|
764
|
+
].join("\n") + "\n",
|
|
765
|
+
);
|
|
766
|
+
const opaqueArgv = spawn(
|
|
767
|
+
"python3",
|
|
768
|
+
["-c", "import time; time.sleep(60)", path.join(root, "node_modules", "@github", "copilot", "npm-loader.js")],
|
|
769
|
+
{ stdio: "ignore" },
|
|
770
|
+
);
|
|
771
|
+
running.push(opaqueArgv);
|
|
772
|
+
try {
|
|
773
|
+
await sleep(400);
|
|
774
|
+
const identified = doctor({ ENTWURF_COPILOT_RECEIVE_PIDS: String(opaqueArgv.pid) });
|
|
775
|
+
const blind = doctor({
|
|
776
|
+
ENTWURF_COPILOT_RECEIVE_PIDS: String(opaqueArgv.pid),
|
|
777
|
+
PYTHONPATH: denyDir,
|
|
778
|
+
ENTWURF_TEST_DENY_CMDLINE_PID: String(opaqueArgv.pid),
|
|
779
|
+
});
|
|
780
|
+
ok(
|
|
781
|
+
"[QK:COPILOT-RECEIVE-DOCTOR-UNREADABLE-ARGV-IS-UNVERIFIABLE] a live process whose argv is REFUSED is UNKNOWN, not absent — the doctor declines to claim that no CLI is running and ends RED, where the same process with a readable argv is judged",
|
|
782
|
+
identified.includes("lack COPILOT_CLI_ENABLED_FEATURE_FLAGS") &&
|
|
783
|
+
blind.includes("UNVERIFIABLE — the argv of 1 live process(es) could not be read") &&
|
|
784
|
+
!blind.includes("no live GitHub Copilot CLI process") &&
|
|
785
|
+
blind.trimEnd().endsWith("[copilot-receive-doctor] FAIL"),
|
|
786
|
+
);
|
|
787
|
+
} finally {
|
|
788
|
+
opaqueArgv.kill("SIGKILL");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
702
791
|
{
|
|
703
792
|
// THE SCANNER'S OWN FAILURE IS THIS DOCTOR'S VERDICT, NOT ITS DEATH. Under
|
|
704
793
|
// `set -euo pipefail`, `x="$(python3 …)"` hands the interpreter's status to the
|
|
@@ -725,10 +814,69 @@ ok(
|
|
|
725
814
|
);
|
|
726
815
|
}
|
|
727
816
|
|
|
728
|
-
// ── 10. the
|
|
817
|
+
// ── 10. the fixture dies with its parent, like the vendor's child does ───────
|
|
818
|
+
// This gate spawns real Node children that hold a referenced timer, so "the gate
|
|
819
|
+
// stopped" and "the gate's children stopped" are two different facts. Production gets
|
|
820
|
+
// the second one from the CLI's bootstrap; the stub loader has to supply it here, or a
|
|
821
|
+
// red run (which ends by throwing, long before any cleanup statement) leaves a process
|
|
822
|
+
// per launch on the host forever. Orphan one deliberately and require it to leave on
|
|
823
|
+
// its own. The intermediate `bash` holds the child for two seconds first: a child whose
|
|
824
|
+
// parent is ALREADY gone when its loader runs leaves at the watchdog's startup check,
|
|
825
|
+
// which proves nothing about a session that was live when the gate died.
|
|
826
|
+
{
|
|
827
|
+
const orphanBox = path.join(root, "box-orphan");
|
|
828
|
+
mkdirSync(orphanBox, { recursive: true });
|
|
829
|
+
const pidFile = path.join(root, "orphan.pid");
|
|
830
|
+
const ORPHAN_SESSION = "cop-recv-orphan";
|
|
831
|
+
spawnSync(
|
|
832
|
+
"bash",
|
|
833
|
+
[
|
|
834
|
+
"-c",
|
|
835
|
+
`${JSON.stringify(process.execPath)} --import ${JSON.stringify(path.join(stubDir, "register.mjs"))} ` +
|
|
836
|
+
`${JSON.stringify(entry)} </dev/null >/dev/null 2>&1 & echo $! > ${JSON.stringify(pidFile)}; sleep 2`,
|
|
837
|
+
],
|
|
838
|
+
{
|
|
839
|
+
env: {
|
|
840
|
+
...process.env,
|
|
841
|
+
HOME: home,
|
|
842
|
+
PI_CODING_AGENT_DIR: store,
|
|
843
|
+
STUB_BOX: orphanBox,
|
|
844
|
+
STUB_SDK: path.join(stubDir, "extension.js"),
|
|
845
|
+
STUB_SESSION_ID: ORPHAN_SESSION,
|
|
846
|
+
SESSION_ID: ORPHAN_SESSION,
|
|
847
|
+
COPILOT_EXTENSION_PARENT_PID: String(process.pid),
|
|
848
|
+
},
|
|
849
|
+
stdio: "ignore",
|
|
850
|
+
},
|
|
851
|
+
);
|
|
852
|
+
const orphanPid = Number(readFileSync(pidFile, "utf8").trim());
|
|
853
|
+
// Non-vacuity: it is only evidence if the process it names actually reached the
|
|
854
|
+
// same live state every other child in this gate reaches. Its log line is written
|
|
855
|
+
// during boot and outlives it, so reading it after the exit is not a race.
|
|
856
|
+
const booted = await waitUntil(() => {
|
|
857
|
+
const f = path.join(store, "meta-bridge-receive-copilot.log");
|
|
858
|
+
return existsSync(f) && readFileSync(f, "utf8").includes(`joined session=${ORPHAN_SESSION}`);
|
|
859
|
+
});
|
|
860
|
+
ok("the orphaned child booted the real unit (so its exit below is not a startup crash)", booted);
|
|
861
|
+
const gone = await waitUntil(() => {
|
|
862
|
+
try {
|
|
863
|
+
process.kill(orphanPid, 0);
|
|
864
|
+
return false;
|
|
865
|
+
} catch {
|
|
866
|
+
return true; // ESRCH — the pid is gone
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
ok(
|
|
870
|
+
"[QK:COPILOT-RECEIVE-STUB-CHILD-DIES-WITH-PARENT] a fixture child whose parent disappeared exits by " +
|
|
871
|
+
"itself — no pid-1 receiver survives a red or interrupted run",
|
|
872
|
+
gone,
|
|
873
|
+
);
|
|
874
|
+
if (!gone) process.kill(orphanPid, "SIGKILL");
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// ── 11. the installer's honest inverse ───────────────────────────────────────
|
|
729
878
|
execFileSync("bash", [path.join(REPO, "run.sh"), "uninstall-copilot-receive"], { env: installEnv, stdio: "pipe" });
|
|
730
879
|
ok("uninstall removes the unit it installed and clears its state", !existsSync(unitDir) && !existsSync(stateFile));
|
|
731
880
|
|
|
732
|
-
|
|
733
|
-
rmSync(root, { recursive: true, force: true });
|
|
881
|
+
reclaimNow();
|
|
734
882
|
console.log(`\n[check-copilot-receive-arm] PASS (${passed} assertions)`);
|
|
@@ -13,6 +13,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
16
17
|
|
|
17
18
|
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
19
|
const SCRIPT = path.join(REPO, "scripts", "copilot-statusline.sh");
|
|
@@ -27,7 +28,8 @@ function ok(label: string, cond: boolean): void {
|
|
|
27
28
|
passed++;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const root = mkdtempSync(path.join(tmpdir(), "entwurf-copilot-statusline."));
|
|
31
|
+
const root = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "entwurf-copilot-statusline.")));
|
|
32
|
+
|
|
31
33
|
const home = path.join(root, "home");
|
|
32
34
|
const store = path.join(root, "meta-sessions");
|
|
33
35
|
mkdirSync(home);
|
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
validateManifest,
|
|
67
67
|
validateManifestSet,
|
|
68
68
|
} from "./lib/mutation-qualify.ts";
|
|
69
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
69
70
|
|
|
70
71
|
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
71
72
|
const MUTANTS_DIR = path.join(REPO_DIR, "scripts", "mutants");
|
|
@@ -135,7 +136,7 @@ function checkVitestAttribution(): void {
|
|
|
135
136
|
],
|
|
136
137
|
});
|
|
137
138
|
|
|
138
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-attribution-selftest-"));
|
|
139
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-attribution-selftest-")));
|
|
139
140
|
const reportPath = path.join(dir, "vitest-report.json");
|
|
140
141
|
try {
|
|
141
142
|
fs.writeFileSync(reportPath, report);
|
|
@@ -422,10 +423,10 @@ if (process.argv.includes("--attribution-self-test")) {
|
|
|
422
423
|
// ═══ Phase 1c — the pipeline over a synthetic fixture repo ══════════════════
|
|
423
424
|
|
|
424
425
|
function buildFixtureOrigin(): { dir: string; externalTarget: string } {
|
|
425
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-fixture-"));
|
|
426
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-fixture-")));
|
|
426
427
|
// A file OUTSIDE the fixture repo, reachable only through a tracked symlink — the
|
|
427
428
|
// P0-1 escape shape: mutating `linked.txt` would write THIS file.
|
|
428
|
-
const externalDir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-external-"));
|
|
429
|
+
const externalDir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-external-")));
|
|
429
430
|
const externalTarget = path.join(externalDir, "external-target.txt");
|
|
430
431
|
fs.writeFileSync(externalTarget, "original-line outside the sandbox\n");
|
|
431
432
|
const write = (rel: string, body: string, mode = 0o644): void => {
|
|
@@ -745,7 +746,7 @@ const quiet = (): void => {};
|
|
|
745
746
|
|
|
746
747
|
// pgroup kill: the grandchild really dies on this host (hardening #6, Linux axis).
|
|
747
748
|
{
|
|
748
|
-
const invocationDir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-pgk-"));
|
|
749
|
+
const invocationDir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-pgk-")));
|
|
749
750
|
const pidFile = path.join(invocationDir, "grandchild.pid");
|
|
750
751
|
const script = path.join(invocationDir, "spawner.sh");
|
|
751
752
|
fs.writeFileSync(script, `#!/usr/bin/env bash\nsleep 300 & echo $! > ${JSON.stringify(pidFile)}\nwait\n`);
|
|
@@ -773,7 +774,7 @@ const quiet = (): void => {};
|
|
|
773
774
|
|
|
774
775
|
// Stale-snapshot sweep: dead-pid residue is reclaimed, a live runner's dir survives.
|
|
775
776
|
{
|
|
776
|
-
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-sweep-"));
|
|
777
|
+
const tmpRoot = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-sweep-")));
|
|
777
778
|
const deadDir = path.join(tmpRoot, "entwurf-qualify-dead1");
|
|
778
779
|
fs.mkdirSync(deadDir);
|
|
779
780
|
fs.writeFileSync(path.join(deadDir, "runner.json"), JSON.stringify({ pid: 999_999_999, startedAt: 0 }));
|
|
@@ -820,7 +821,7 @@ let manifestCount: number;
|
|
|
820
821
|
// Extend this inventory and the manifests TOGETHER, never silently.
|
|
821
822
|
const EXPECTED_LANE_MUTANTS: Record<string, number> = {
|
|
822
823
|
"acp-augment": 10,
|
|
823
|
-
"acp-cortex":
|
|
824
|
+
"acp-cortex": 14,
|
|
824
825
|
"acp-launch-namespace": 2,
|
|
825
826
|
"acp-overlay": 1,
|
|
826
827
|
"acp-prompt-lifecycle": 15,
|
|
@@ -833,7 +834,7 @@ let manifestCount: number;
|
|
|
833
834
|
"capability-cache": 3,
|
|
834
835
|
"copilot-birth": 19,
|
|
835
836
|
"copilot-launch": 14,
|
|
836
|
-
"copilot-receive":
|
|
837
|
+
"copilot-receive": 20,
|
|
837
838
|
"fresh-cut": 3,
|
|
838
839
|
"gate-qualification": 2,
|
|
839
840
|
"meta-facts": 4,
|
|
@@ -847,7 +848,7 @@ let manifestCount: number;
|
|
|
847
848
|
"pack-install": 2,
|
|
848
849
|
"pi-package-ownership": 6,
|
|
849
850
|
"mux-resume-call": 12,
|
|
850
|
-
"omp-birth":
|
|
851
|
+
"omp-birth": 13,
|
|
851
852
|
"omp-fresh": 24,
|
|
852
853
|
"omp-receive": 11,
|
|
853
854
|
"probe-ordering": 1,
|
|
@@ -855,7 +856,7 @@ let manifestCount: number;
|
|
|
855
856
|
"resume-args": 6,
|
|
856
857
|
"resume-launch-identity": 6,
|
|
857
858
|
"self-address": 5,
|
|
858
|
-
"setup-verdict":
|
|
859
|
+
"setup-verdict": 14,
|
|
859
860
|
"source-install": 2,
|
|
860
861
|
"v2-surface": 7,
|
|
861
862
|
"v2-visible-resume": 17,
|
|
@@ -45,6 +45,7 @@ import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSyn
|
|
|
45
45
|
import { tmpdir } from "node:os";
|
|
46
46
|
import * as path from "node:path";
|
|
47
47
|
import { fileURLToPath } from "node:url";
|
|
48
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
48
49
|
|
|
49
50
|
let passed = 0;
|
|
50
51
|
function ok(label: string, cond: boolean): void {
|
|
@@ -272,7 +273,7 @@ function hookEnv(agentRoot: string, pluginRoot: string): NodeJS.ProcessEnv {
|
|
|
272
273
|
}
|
|
273
274
|
|
|
274
275
|
function driveHook(label: string, pluginRoot: string, nativeSessionId: string): void {
|
|
275
|
-
const agentRoot = mkdtempSync(path.join(tmpdir(), "psa-hook-topology-"));
|
|
276
|
+
const agentRoot = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-topology-")));
|
|
276
277
|
const leaf = leafOf("SessionStart");
|
|
277
278
|
const exe = resolveEl(leaf.command ?? "", pluginRoot);
|
|
278
279
|
const argv = (leaf.args ?? []).map((a) => resolveEl(a, pluginRoot));
|
|
@@ -317,7 +318,7 @@ function driveHook(label: string, pluginRoot: string, nativeSessionId: string):
|
|
|
317
318
|
}
|
|
318
319
|
|
|
319
320
|
{
|
|
320
|
-
const plain = mkdtempSync(path.join(tmpdir(), "psa-hook-bundle-"));
|
|
321
|
+
const plain = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-bundle-")));
|
|
321
322
|
makeBundle(plain);
|
|
322
323
|
driveHook("exec direct topology", plain, "native-exec-direct");
|
|
323
324
|
}
|
|
@@ -329,7 +330,7 @@ function driveHook(label: string, pluginRoot: string, nativeSessionId: string):
|
|
|
329
330
|
// path was pasted into a command string and would have been word-split or
|
|
330
331
|
// command-substituted; under exec form each element is one opaque argv slot.
|
|
331
332
|
{
|
|
332
|
-
const holder = mkdtempSync(path.join(tmpdir(), "psa-hook-meta-"));
|
|
333
|
+
const holder = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-meta-")));
|
|
333
334
|
const nasty = path.join(holder, "plug in $HOME `id` ;& dir");
|
|
334
335
|
makeBundle(nasty);
|
|
335
336
|
ok("metachar fixture root really contains space/$/backtick/;&", /[ $`;&]/.test(path.basename(nasty)));
|
|
@@ -345,9 +346,9 @@ function driveHook(label: string, pluginRoot: string, nativeSessionId: string):
|
|
|
345
346
|
// it. The launcher's provenance token is what makes this fail CLOSED — the record still
|
|
346
347
|
// lands (best-effort, as always), but no presence is claimed that cannot be backed.
|
|
347
348
|
{
|
|
348
|
-
const pluginRoot = mkdtempSync(path.join(tmpdir(), "psa-hook-oldcmd-"));
|
|
349
|
+
const pluginRoot = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-oldcmd-")));
|
|
349
350
|
makeBundle(pluginRoot);
|
|
350
|
-
const agentRoot = mkdtempSync(path.join(tmpdir(), "psa-hook-oldcmd-agent-"));
|
|
351
|
+
const agentRoot = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-oldcmd-agent-")));
|
|
351
352
|
const result = spawnSync(process.execPath, [path.join(pluginRoot, "meta-bridge-hook.ts")], {
|
|
352
353
|
encoding: "utf8",
|
|
353
354
|
input: JSON.stringify({
|
|
@@ -510,12 +510,13 @@ const operatorCmds = [...targets].filter(([cmd, ts]) => !isDevGate(cmd) && ts.le
|
|
|
510
510
|
`${skillRel}: missing name, four authority modes, prerelease contract, or English/ASCII-only surface`,
|
|
511
511
|
);
|
|
512
512
|
ok(
|
|
513
|
-
"S7g: the executable ASCII CI oracle binds headSha and all
|
|
513
|
+
"S7g: the executable ASCII CI oracle binds headSha and all four required jobs",
|
|
514
514
|
ciOracle === null ||
|
|
515
515
|
(ciOracle.includes("headSha") &&
|
|
516
516
|
ciOracle.includes('"check"') &&
|
|
517
517
|
ciOracle.includes('"install-surface"') &&
|
|
518
518
|
ciOracle.includes('"artifact-consumer"') &&
|
|
519
|
+
ciOracle.includes('"macos-install-surface"') &&
|
|
519
520
|
ciOracleMode === "100755" &&
|
|
520
521
|
oracleIsAscii),
|
|
521
522
|
`${ciOracleRel}: missing executable mode, exact-SHA classification, required jobs, or ASCII-only content`,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type MetaIdentity,
|
|
34
34
|
serializeMetaIdentity,
|
|
35
35
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
36
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
36
37
|
|
|
37
38
|
let passed = 0;
|
|
38
39
|
function ok(label: string, cond: boolean): void {
|
|
@@ -62,7 +63,7 @@ function record(gardenId: string, over: Partial<MetaIdentity> = {}): string {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
function store(files: Record<string, string | Buffer>): string {
|
|
65
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-"));
|
|
66
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-")));
|
|
66
67
|
for (const [name, body] of Object.entries(files)) fs.writeFileSync(path.join(dir, name), body);
|
|
67
68
|
return dir;
|
|
68
69
|
}
|
|
@@ -148,7 +149,7 @@ function parseProjection(run: CliRun, label: string): Projection {
|
|
|
148
149
|
|
|
149
150
|
// ── drift / symlink / invalid UTF-8 are in-band defects, exit 0 ─────────────
|
|
150
151
|
{
|
|
151
|
-
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-outside-"));
|
|
152
|
+
const outside = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-outside-")));
|
|
152
153
|
fs.writeFileSync(path.join(outside, "foreign.meta.json"), record(GID_C));
|
|
153
154
|
const dir = store({
|
|
154
155
|
[`${GID_A}.meta.json`]: record(GID_A),
|
|
@@ -206,7 +207,8 @@ function parseProjection(run: CliRun, label: string): Projection {
|
|
|
206
207
|
|
|
207
208
|
// ── missing store = readable EMPTY store ────────────────────────────────────
|
|
208
209
|
{
|
|
209
|
-
const
|
|
210
|
+
const goneRoot = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-")));
|
|
211
|
+
const gone = path.join(goneRoot, "never-created");
|
|
210
212
|
const facts = parseProjection(cli([gone]), "missing store");
|
|
211
213
|
ok(
|
|
212
214
|
"a store that does not exist is a readable empty store (ENOENT only)",
|
|
@@ -216,7 +218,7 @@ function parseProjection(run: CliRun, label: string): Projection {
|
|
|
216
218
|
|
|
217
219
|
// ── unreadable store = exit 3, NO JSON ──────────────────────────────────────
|
|
218
220
|
{
|
|
219
|
-
const base = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-"));
|
|
221
|
+
const base = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-")));
|
|
220
222
|
const file = path.join(base, "not-a-dir");
|
|
221
223
|
fs.writeFileSync(file, "plain file");
|
|
222
224
|
const run = cli([path.join(file, "store")]); // ENOTDIR: the path runs THROUGH a regular file
|
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
upsertMetaSession,
|
|
71
71
|
writeMetaReceiverMarker,
|
|
72
72
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
73
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
73
74
|
|
|
74
75
|
let passed = 0;
|
|
75
76
|
function ok(label: string, cond: boolean): void {
|
|
@@ -112,9 +113,9 @@ function resolveEl(value: string, pluginRoot: string): string {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
// ── one sandbox for the whole switch story (the pid is what ties it together) ──
|
|
115
|
-
const AGENT_ROOT = mkdtempSync(path.join(tmpdir(), "psa-hook-switch-agent-"));
|
|
116
|
-
const PLUGIN_ROOT = mkdtempSync(path.join(tmpdir(), "psa-hook-switch-plugin-"));
|
|
117
|
-
const CWD = mkdtempSync(path.join(tmpdir(), "psa-hook-switch-cwd-"));
|
|
116
|
+
const AGENT_ROOT = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-switch-agent-")));
|
|
117
|
+
const PLUGIN_ROOT = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-switch-plugin-")));
|
|
118
|
+
const CWD = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-switch-cwd-")));
|
|
118
119
|
makeBundle(PLUGIN_ROOT);
|
|
119
120
|
|
|
120
121
|
const ROOTS = {
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
serializeMetaIdentity,
|
|
53
53
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
54
54
|
import { birthPiCitizen } from "../pi-extensions/lib/pi-citizen-birth.ts";
|
|
55
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
55
56
|
|
|
56
57
|
let passed = 0;
|
|
57
58
|
function ok(label: string, cond: boolean): void {
|
|
@@ -103,7 +104,7 @@ const v2Body = `${JSON.stringify(
|
|
|
103
104
|
|
|
104
105
|
/** Run `body` against a store dir seeded by `seed`, then remove it. */
|
|
105
106
|
function withStore(seed: (dir: string) => void, body: (dir: string) => void): void {
|
|
106
|
-
const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-case-"));
|
|
107
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-case-")));
|
|
107
108
|
try {
|
|
108
109
|
seed(dir);
|
|
109
110
|
body(dir);
|
|
@@ -129,7 +130,7 @@ const record = (dir: string, gardenId: string, nativeSessionId: string): void =>
|
|
|
129
130
|
write(dir, `${gardenId}.meta.json`, serializeMetaIdentity(identity(gardenId, nativeSessionId)));
|
|
130
131
|
const recordCount = (dir: string): number => fs.readdirSync(dir).filter((f) => f.endsWith(".meta.json")).length;
|
|
131
132
|
|
|
132
|
-
const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-"));
|
|
133
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-")));
|
|
133
134
|
try {
|
|
134
135
|
record(dir, GID_A, "native-a");
|
|
135
136
|
record(dir, GID_B, "native-b");
|
|
@@ -355,7 +356,7 @@ withStore(
|
|
|
355
356
|
// entries with the right kind. A pure-only proof would have passed while the fs path
|
|
356
357
|
// still did a bare-name readdir and followed symlinks — which is exactly what shipped.
|
|
357
358
|
{
|
|
358
|
-
const tmp = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-addr-"));
|
|
359
|
+
const tmp = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-addr-")));
|
|
359
360
|
try {
|
|
360
361
|
// A symlink whose target lives OUTSIDE the store and holds the same native id.
|
|
361
362
|
const dir = path.join(tmp, "symlink-store");
|
|
@@ -444,7 +445,7 @@ withStore(
|
|
|
444
445
|
// into foreign bytes while every "is it a symlink" test on a settled store stayed green.
|
|
445
446
|
// A plain already-a-symlink read cannot fail that way, so it cannot prove this.
|
|
446
447
|
{
|
|
447
|
-
const tmp = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-toctou-"));
|
|
448
|
+
const tmp = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-toctou-")));
|
|
448
449
|
try {
|
|
449
450
|
const dir = path.join(tmp, "store");
|
|
450
451
|
const outside = path.join(tmp, "outside");
|
|
@@ -700,7 +701,7 @@ withStore(
|
|
|
700
701
|
// where they live and a probe of this directory cannot certify them.
|
|
701
702
|
withStore(
|
|
702
703
|
(dir) => {
|
|
703
|
-
const outside = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-outside-"));
|
|
704
|
+
const outside = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-outside-")));
|
|
704
705
|
fs.writeFileSync(path.join(outside, "real.json"), serializeMetaIdentity(identity(GID_C, "native-symlinked")));
|
|
705
706
|
fs.symlinkSync(path.join(outside, "real.json"), path.join(dir, `${GID_C}.meta.json`));
|
|
706
707
|
},
|
|
@@ -889,7 +890,7 @@ withStore(
|
|
|
889
890
|
// clean "no such citizen" — the fail-open shape (2026-07-25 fresh-eyes review). ENOENT
|
|
890
891
|
// alone is absence; a broken store SHAPE (ENOTDIR here) is a failure to inspect.
|
|
891
892
|
{
|
|
892
|
-
const tmp = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-shape-"));
|
|
893
|
+
const tmp = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-shape-")));
|
|
893
894
|
try {
|
|
894
895
|
const brokenStore = path.join(tmp, "store-is-actually-a-file");
|
|
895
896
|
fs.writeFileSync(brokenStore, "not a store\n");
|
|
@@ -927,7 +928,7 @@ withStore(
|
|
|
927
928
|
// — the doctor and the install preflight would call an unreadable host clean. ENOENT
|
|
928
929
|
// alone is an absent store (a host that never had a generation is not broken).
|
|
929
930
|
{
|
|
930
|
-
const tmp = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-unreadable-"));
|
|
931
|
+
const tmp = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-unreadable-")));
|
|
931
932
|
try {
|
|
932
933
|
ok(
|
|
933
934
|
"an absent store still certifies as empty (ENOENT alone)",
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
startKeyScheme,
|
|
42
42
|
writeMetaReceiverMarker,
|
|
43
43
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
44
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
44
45
|
|
|
45
46
|
let passed = 0;
|
|
46
47
|
function ok(label: string, cond: boolean): void {
|
|
@@ -50,7 +51,7 @@ function ok(label: string, cond: boolean): void {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
53
|
-
const DIR = mkdtempSync(path.join(tmpdir(), "psa-meta-receivers-"));
|
|
54
|
+
const DIR = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-meta-receivers-")));
|
|
54
55
|
const GARDEN = "20260614T120000-aaaaaa";
|
|
55
56
|
|
|
56
57
|
// ── write → read round-trip, garden-id keyed ────────────────────────────────
|