paperlint 10.0.0 → 10.1.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/CONTRIBUTING.md +16 -4
- package/dist/eslint-rules/js-needs-reason.d.ts +55 -0
- package/dist/eslint-rules/js-needs-reason.d.ts.map +1 -0
- package/dist/eslint-rules/js-needs-reason.js +35 -0
- package/dist/eslint-rules/js-needs-reason.js.map +1 -0
- package/dist/package-dirs.d.ts +6 -0
- package/dist/package-dirs.d.ts.map +1 -1
- package/dist/package-dirs.js +6 -0
- package/dist/package-dirs.js.map +1 -1
- package/dist/skills/label-agent-runs/scripts/labels-commands.d.ts +13 -0
- package/dist/skills/label-agent-runs/scripts/labels.d.ts +2 -0
- package/dist/skills/run-agent-grid/scripts/grid-guard-commands.d.ts +20 -0
- package/dist/skills/run-agent-grid/scripts/grid-guard.d.ts +2 -0
- package/docs/skills.md +2 -0
- package/docs/testing.md +13 -0
- package/eslint-rules/js-needs-reason.test.ts +141 -0
- package/eslint-rules/js-needs-reason.ts +77 -0
- package/eslint.config.mjs +22 -2
- package/lib/skill-trigger-cases.test.ts +211 -0
- package/package.json +8 -4
- package/scripts/coverage-config.test.ts +5 -3
- package/skills/build-benchmark/SKILL.md +4 -1
- package/skills/build-benchmark/SKILL.md.spec.ts +3 -0
- package/skills/build-label-page/SKILL.md +3 -1
- package/skills/build-label-page/SKILL.md.spec.ts +2 -0
- package/skills/label-agent-runs/SKILL.md +103 -0
- package/skills/label-agent-runs/SKILL.md.spec.ts +106 -0
- package/skills/label-agent-runs/label-agent-runs.eval.ts +55 -0
- package/skills/label-agent-runs/label-agent-runs.harness.ts +123 -0
- package/skills/label-agent-runs/references/formats.md +64 -0
- package/skills/label-agent-runs/scripts/labels-commands.js +13 -0
- package/skills/label-agent-runs/scripts/labels-commands.ts +13 -0
- package/skills/label-agent-runs/scripts/labels.js +396 -0
- package/skills/label-agent-runs/scripts/labels.test.ts +538 -0
- package/skills/label-agent-runs/scripts/labels.ts +501 -0
- package/skills/paper-pipeline/SKILL.md +8 -1
- package/skills/paper-pipeline/SKILL.md.spec.ts +7 -0
- package/skills/run-agent-grid/SKILL.md +181 -0
- package/skills/run-agent-grid/SKILL.md.spec.ts +184 -0
- package/skills/run-agent-grid/references/failure-catalog.md +36 -0
- package/skills/run-agent-grid/references/registry-and-ledger.md +142 -0
- package/skills/run-agent-grid/run-agent-grid.eval.ts +59 -0
- package/skills/run-agent-grid/run-agent-grid.harness.ts +115 -0
- package/skills/run-agent-grid/scripts/grid-guard-commands.js +18 -0
- package/skills/run-agent-grid/scripts/grid-guard-commands.ts +21 -0
- package/skills/run-agent-grid/scripts/grid-guard.js +700 -0
- package/skills/run-agent-grid/scripts/grid-guard.test.ts +1090 -0
- package/skills/run-agent-grid/scripts/grid-guard.ts +869 -0
- package/src/package-dirs.ts +8 -0
- package/tsconfig.base.json +22 -0
- package/tsconfig.json +6 -24
- package/tsconfig.pkg.json +3 -1
- package/tsconfig.skills.json +14 -0
- package/tsconfig.src.json +12 -0
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grid-guard.ts, run as a process the way a driver or a commit hook runs it. Each check is shown
|
|
3
|
+
* BOTH ways: silent (exit 0) on correct input, and firing on a planted defect — a live lock
|
|
4
|
+
* holder, a stale one, two attempts sharing a root, an uncommitted registry change, a staged key.
|
|
5
|
+
* Every expectation is the whole result: exit code, stdout and stderr.
|
|
6
|
+
*
|
|
7
|
+
* No model, no network. Needs `git`.
|
|
8
|
+
*/
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { hostname } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { onTestFinished, test } from "vitest";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import {
|
|
17
|
+
parseJson,
|
|
18
|
+
runNode,
|
|
19
|
+
useTempDir,
|
|
20
|
+
writeTree,
|
|
21
|
+
type ScriptResult,
|
|
22
|
+
} from "../../../test/support.ts";
|
|
23
|
+
|
|
24
|
+
const SCRIPT = join(import.meta.dirname, "grid-guard.ts");
|
|
25
|
+
const WORK = useTempDir("grid-guard-");
|
|
26
|
+
const GIT_ENV = {
|
|
27
|
+
GIT_AUTHOR_NAME: "t",
|
|
28
|
+
GIT_AUTHOR_EMAIL: "t@example.invalid",
|
|
29
|
+
GIT_COMMITTER_NAME: "t",
|
|
30
|
+
GIT_COMMITTER_EMAIL: "t@example.invalid",
|
|
31
|
+
};
|
|
32
|
+
const guard = (args: readonly string[], cwd?: string): ScriptResult =>
|
|
33
|
+
runNode(SCRIPT, args, { cwd: cwd ?? WORK, env: GIT_ENV });
|
|
34
|
+
const ok = (stdout: string): ScriptResult => ({
|
|
35
|
+
status: 0,
|
|
36
|
+
stdout: `${stdout}\n`,
|
|
37
|
+
stderr: "",
|
|
38
|
+
});
|
|
39
|
+
const no = (status: number, stderr: string): ScriptResult => ({
|
|
40
|
+
status,
|
|
41
|
+
stdout: "",
|
|
42
|
+
stderr: `grid-guard: ${stderr}\n`,
|
|
43
|
+
});
|
|
44
|
+
const git = (cwd: string, ...args: string[]): string => {
|
|
45
|
+
const r = spawnSync("git", args, {
|
|
46
|
+
cwd,
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
env: { ...process.env, ...GIT_ENV },
|
|
49
|
+
});
|
|
50
|
+
assert.equal(r.status, 0, r.stderr);
|
|
51
|
+
return r.stdout;
|
|
52
|
+
};
|
|
53
|
+
const repo = (
|
|
54
|
+
name: string,
|
|
55
|
+
files: Readonly<Record<string, string>>,
|
|
56
|
+
): string => {
|
|
57
|
+
const dir = writeTree(join(WORK, name), files);
|
|
58
|
+
git(dir, "init", "-q");
|
|
59
|
+
git(dir, "add", ".");
|
|
60
|
+
git(dir, "commit", "-q", "-m", "init");
|
|
61
|
+
return dir;
|
|
62
|
+
};
|
|
63
|
+
const lockFile = (root: string): Record<string, unknown> =>
|
|
64
|
+
z
|
|
65
|
+
.record(z.string(), z.unknown())
|
|
66
|
+
.parse(parseJson(readFileSync(`${root}.lock`, "utf8")));
|
|
67
|
+
const USAGE =
|
|
68
|
+
"usage: grid-guard.js <lock|release|gate|overlap|env-probe|scan-keys|precommit> … (see the header of this file)";
|
|
69
|
+
|
|
70
|
+
test("usage: no command, an unknown one, an unknown flag, a missing option, a missing path", () => {
|
|
71
|
+
assert.deepEqual(guard([]), no(2, USAGE));
|
|
72
|
+
assert.deepEqual(guard(["fly"]), no(2, USAGE));
|
|
73
|
+
assert.deepEqual(
|
|
74
|
+
guard(["lock", "x", "--owner", "a", "--colour", "red"]),
|
|
75
|
+
no(
|
|
76
|
+
2,
|
|
77
|
+
`Unknown option '--colour'. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- "--colour"\n${USAGE}`,
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
assert.deepEqual(
|
|
81
|
+
guard(["lock", "x"]),
|
|
82
|
+
no(2, `lock: needs --owner\n${USAGE}`),
|
|
83
|
+
);
|
|
84
|
+
assert.deepEqual(
|
|
85
|
+
guard(["lock", "--owner", "a"]),
|
|
86
|
+
no(2, `lock: wrong number of paths\n${USAGE}`),
|
|
87
|
+
);
|
|
88
|
+
assert.deepEqual(
|
|
89
|
+
guard(["scan-keys"]),
|
|
90
|
+
no(2, `scan-keys: wrong number of paths\n${USAGE}`),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("lock and release: a free root is locked; a second driver is refused while the holder lives", () => {
|
|
95
|
+
const root = join(WORK, "wc-live");
|
|
96
|
+
// This test's own process is the holder: alive for as long as the assertion runs.
|
|
97
|
+
const first = guard([
|
|
98
|
+
"lock",
|
|
99
|
+
root,
|
|
100
|
+
"--owner",
|
|
101
|
+
"driver-a",
|
|
102
|
+
"--pid",
|
|
103
|
+
String(process.pid),
|
|
104
|
+
"--attached",
|
|
105
|
+
]);
|
|
106
|
+
assert.deepEqual(
|
|
107
|
+
first,
|
|
108
|
+
ok(`locked ${root}.lock for driver-a (pid ${String(process.pid)})`),
|
|
109
|
+
);
|
|
110
|
+
const held = lockFile(root);
|
|
111
|
+
assert.deepEqual(held, {
|
|
112
|
+
owner: "driver-a",
|
|
113
|
+
pid: process.pid,
|
|
114
|
+
host: hostname(),
|
|
115
|
+
at: held.at,
|
|
116
|
+
});
|
|
117
|
+
assert.deepEqual(
|
|
118
|
+
guard(["lock", root, "--owner", "driver-b", "--attached"]),
|
|
119
|
+
no(
|
|
120
|
+
1,
|
|
121
|
+
`${root}.lock is held by driver-a (pid ${String(process.pid)} on ${hostname()} since ${String(held.at)}). Wait for it, or give this driver its own working-copy root.`,
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
assert.deepEqual(
|
|
125
|
+
lockFile(root),
|
|
126
|
+
held,
|
|
127
|
+
"the refused driver must not touch the lock",
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("lock and release: a lock whose process is gone on this host is taken over; one from another host is not", () => {
|
|
132
|
+
const dead = spawnSync(process.execPath, ["-e", ""]).pid;
|
|
133
|
+
const root = join(WORK, "wc-stale");
|
|
134
|
+
writeTree(WORK, {
|
|
135
|
+
"wc-stale.lock": JSON.stringify({
|
|
136
|
+
owner: "crashed",
|
|
137
|
+
pid: dead,
|
|
138
|
+
host: hostname(),
|
|
139
|
+
at: "x",
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
assert.deepEqual(
|
|
143
|
+
guard([
|
|
144
|
+
"lock",
|
|
145
|
+
root,
|
|
146
|
+
"--owner",
|
|
147
|
+
"driver-b",
|
|
148
|
+
"--pid",
|
|
149
|
+
String(process.pid),
|
|
150
|
+
"--attached",
|
|
151
|
+
]),
|
|
152
|
+
ok(
|
|
153
|
+
`took over a stale lock on ${root}.lock (holder crashed, pid ${String(dead)}, is gone)`,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
assert.equal(lockFile(root).owner, "driver-b");
|
|
157
|
+
|
|
158
|
+
const far = join(WORK, "wc-far");
|
|
159
|
+
writeTree(WORK, {
|
|
160
|
+
"wc-far.lock": JSON.stringify({
|
|
161
|
+
owner: "remote",
|
|
162
|
+
pid: dead,
|
|
163
|
+
host: "elsewhere",
|
|
164
|
+
at: "x",
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
assert.deepEqual(
|
|
168
|
+
guard(["lock", far, "--owner", "driver-b", "--attached"]),
|
|
169
|
+
no(
|
|
170
|
+
1,
|
|
171
|
+
`${far}.lock is held by remote (pid ${String(dead)} on elsewhere since x). Wait for it, or give this driver its own working-copy root.`,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("lock and release: without --pid the lock records the caller's process", () => {
|
|
177
|
+
const root = join(WORK, "wc-ppid");
|
|
178
|
+
const r = guard(["lock", root, "--owner", "a", "--attached"]);
|
|
179
|
+
assert.equal(r.status, 0, r.stderr);
|
|
180
|
+
// The caller here is this test process, which spawned node directly.
|
|
181
|
+
assert.equal(lockFile(root).pid, process.pid);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("lock and release: release: only by the owner, only when held", () => {
|
|
185
|
+
const root = join(WORK, "wc-release");
|
|
186
|
+
assert.equal(
|
|
187
|
+
guard([
|
|
188
|
+
"lock",
|
|
189
|
+
root,
|
|
190
|
+
"--owner",
|
|
191
|
+
"a",
|
|
192
|
+
"--pid",
|
|
193
|
+
String(process.pid),
|
|
194
|
+
"--attached",
|
|
195
|
+
]).status,
|
|
196
|
+
0,
|
|
197
|
+
);
|
|
198
|
+
assert.deepEqual(
|
|
199
|
+
guard(["release", root, "--owner", "b"]),
|
|
200
|
+
no(1, `${root}.lock is held by a, not b`),
|
|
201
|
+
);
|
|
202
|
+
assert.deepEqual(
|
|
203
|
+
guard(["release", root, "--owner", "a"]),
|
|
204
|
+
ok(`released ${root}.lock`),
|
|
205
|
+
);
|
|
206
|
+
assert.equal(existsSync(`${root}.lock`), false);
|
|
207
|
+
assert.deepEqual(
|
|
208
|
+
guard(["release", root, "--owner", "a"]),
|
|
209
|
+
no(1, `${root}.lock is not held`),
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("lock and release: a root whose parent does not exist is an error, not a lock", () => {
|
|
214
|
+
const root = join(WORK, "no-such-dir", "wc");
|
|
215
|
+
const r = guard(["lock", root, "--owner", "a", "--attached"]);
|
|
216
|
+
assert.deepEqual(
|
|
217
|
+
r,
|
|
218
|
+
no(1, `ENOENT: no such file or directory, open '${root}.lock'`),
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
/** A sleeping node process, in a session of its own when `detached`; killed after the test. */
|
|
223
|
+
const sleeper = (detached: boolean): number => {
|
|
224
|
+
const child = spawn(process.execPath, ["-e", "setTimeout(() => {}, 60000)"], {
|
|
225
|
+
detached,
|
|
226
|
+
stdio: "ignore",
|
|
227
|
+
});
|
|
228
|
+
child.unref();
|
|
229
|
+
onTestFinished(() => {
|
|
230
|
+
child.kill();
|
|
231
|
+
});
|
|
232
|
+
return Number(child.pid);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/** What a lock by `pid` prints on stderr: nothing where `/proc` can be read, a warning where not. */
|
|
236
|
+
const procWarning = (pid: number, proc = "/proc"): string =>
|
|
237
|
+
existsSync(join(proc, String(pid), "stat"))
|
|
238
|
+
? ""
|
|
239
|
+
: `grid-guard: warning: cannot read ${join(proc, String(pid), "stat")} here, so whether pid ${String(pid)} is detached is not checked\n`;
|
|
240
|
+
|
|
241
|
+
test("lock: a detached holder (its own session) takes the lock; an attached one is refused", () => {
|
|
242
|
+
const free = sleeper(true);
|
|
243
|
+
const root = join(WORK, "wc-detached");
|
|
244
|
+
assert.deepEqual(
|
|
245
|
+
guard(["lock", root, "--owner", "driver-a", "--pid", String(free)]),
|
|
246
|
+
{
|
|
247
|
+
...ok(`locked ${root}.lock for driver-a (pid ${String(free)})`),
|
|
248
|
+
stderr: procWarning(free),
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
// The session check needs /proc; where there is none (macOS) it warns and passes, so the
|
|
252
|
+
// refusal below is a Linux fact. The no-/proc branch is pinned in the next test on every platform.
|
|
253
|
+
if (!existsSync("/proc/self/stat")) return;
|
|
254
|
+
const tied = sleeper(false);
|
|
255
|
+
const other = join(WORK, "wc-attached");
|
|
256
|
+
const r = guard([
|
|
257
|
+
"lock",
|
|
258
|
+
other,
|
|
259
|
+
"--owner",
|
|
260
|
+
"driver-b",
|
|
261
|
+
"--pid",
|
|
262
|
+
String(tied),
|
|
263
|
+
]);
|
|
264
|
+
assert.equal(r.status, 1);
|
|
265
|
+
assert.equal(r.stdout, "");
|
|
266
|
+
assert.match(
|
|
267
|
+
r.stderr,
|
|
268
|
+
new RegExp(
|
|
269
|
+
`^grid-guard: pid ${String(tied)} is not detached \\(it runs in session \\d+\\), so it dies with the session that started it\\. Start the driver as \`setsid nohup <driver> > driver\\.log 2>&1 &\` and lock with --pid \\$\\$ inside it, or pass --attached for a short run\\.\\n$`,
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
assert.equal(existsSync(`${other}.lock`), false);
|
|
273
|
+
// Said out loud, an attached holder is allowed: a short run that the session outlives.
|
|
274
|
+
assert.deepEqual(
|
|
275
|
+
guard([
|
|
276
|
+
"lock",
|
|
277
|
+
other,
|
|
278
|
+
"--owner",
|
|
279
|
+
"driver-b",
|
|
280
|
+
"--pid",
|
|
281
|
+
String(tied),
|
|
282
|
+
"--attached",
|
|
283
|
+
]),
|
|
284
|
+
ok(`locked ${other}.lock for driver-b (pid ${String(tied)})`),
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("lock: a pid that is no process is refused with or without /proc; without /proc a live one warns and passes", () => {
|
|
289
|
+
const dead = spawnSync(process.execPath, ["-e", ""]).pid;
|
|
290
|
+
const root = join(WORK, "wc-nopid");
|
|
291
|
+
const noProc = { GRID_GUARD_PROC: join(WORK, "no-proc-here") };
|
|
292
|
+
assert.deepEqual(
|
|
293
|
+
guard(["lock", root, "--owner", "a", "--pid", String(dead)]),
|
|
294
|
+
no(1, `pid ${String(dead)} is not a live process`),
|
|
295
|
+
);
|
|
296
|
+
// Liveness is the kernel's answer, not /proc's: a dead pid is refused where /proc is missing too.
|
|
297
|
+
assert.deepEqual(
|
|
298
|
+
runNode(SCRIPT, ["lock", root, "--owner", "a", "--pid", String(dead)], {
|
|
299
|
+
cwd: WORK,
|
|
300
|
+
env: noProc,
|
|
301
|
+
}),
|
|
302
|
+
no(1, `pid ${String(dead)} is not a live process`),
|
|
303
|
+
);
|
|
304
|
+
const live = sleeper(false);
|
|
305
|
+
assert.deepEqual(
|
|
306
|
+
runNode(SCRIPT, ["lock", root, "--owner", "a", "--pid", String(live)], {
|
|
307
|
+
cwd: WORK,
|
|
308
|
+
env: noProc,
|
|
309
|
+
}),
|
|
310
|
+
{
|
|
311
|
+
...ok(`locked ${root}.lock for a (pid ${String(live)})`),
|
|
312
|
+
stderr: procWarning(live, join(WORK, "no-proc-here")),
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const REGISTRY = {
|
|
318
|
+
author: "the-author",
|
|
319
|
+
draw: { seed: 20240101, population: "every unit of the measured kind" },
|
|
320
|
+
arms: ["control", "treatment"],
|
|
321
|
+
seeds: [1, 2],
|
|
322
|
+
budget: { cap: 10, currency: "USD" },
|
|
323
|
+
models: [
|
|
324
|
+
{
|
|
325
|
+
id: "m1",
|
|
326
|
+
smoke: { timeoutSec: 1800, costPerRun: 2, readBy: "the-author" },
|
|
327
|
+
},
|
|
328
|
+
{ id: "m2" },
|
|
329
|
+
],
|
|
330
|
+
units: [
|
|
331
|
+
{
|
|
332
|
+
id: "u1",
|
|
333
|
+
role: "counted",
|
|
334
|
+
requirement: "the unit as the author asked for it, verbatim",
|
|
335
|
+
approval: {
|
|
336
|
+
by: "the-author",
|
|
337
|
+
at: "2026-01-02T10:00:00Z",
|
|
338
|
+
channel: "chat",
|
|
339
|
+
},
|
|
340
|
+
proof: { toolVisible: true, firesOnPinned: true, acceptsReference: true },
|
|
341
|
+
},
|
|
342
|
+
{ id: "u-smoke", role: "smoke" },
|
|
343
|
+
{
|
|
344
|
+
id: "u-agent",
|
|
345
|
+
role: "counted",
|
|
346
|
+
approval: { by: "the-agent", at: "x", channel: "x" },
|
|
347
|
+
proof: {},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
id: "u-out",
|
|
351
|
+
role: "excluded",
|
|
352
|
+
reason: "the pinned state does not build",
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
};
|
|
356
|
+
const CELL = [
|
|
357
|
+
"--unit",
|
|
358
|
+
"u1",
|
|
359
|
+
"--arm",
|
|
360
|
+
"control",
|
|
361
|
+
"--model",
|
|
362
|
+
"m1",
|
|
363
|
+
"--seed",
|
|
364
|
+
"1",
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
const dir = repo("gate", { "grid.json": JSON.stringify(REGISTRY, null, 2) });
|
|
368
|
+
const reg = join(dir, "grid.json");
|
|
369
|
+
const ledger = join(dir, "attempts.jsonl");
|
|
370
|
+
/** `gate` on `cell`; an attempt id `x0` unless the cell's arguments name one. */
|
|
371
|
+
const gate = (cell: readonly string[], ledgerPath = ledger): ScriptResult =>
|
|
372
|
+
guard([
|
|
373
|
+
"gate",
|
|
374
|
+
"--registry",
|
|
375
|
+
reg,
|
|
376
|
+
"--ledger",
|
|
377
|
+
ledgerPath,
|
|
378
|
+
...cell,
|
|
379
|
+
...(cell.includes("--attempt") ? [] : ["--attempt", "x0"]),
|
|
380
|
+
]);
|
|
381
|
+
/** The ledger's records, parsed. */
|
|
382
|
+
const records = (path: string): Record<string, unknown>[] =>
|
|
383
|
+
readFileSync(path, "utf8")
|
|
384
|
+
.split("\n")
|
|
385
|
+
.filter(Boolean)
|
|
386
|
+
.map((l) => z.record(z.string(), z.unknown()).parse(JSON.parse(l)));
|
|
387
|
+
|
|
388
|
+
test("gate: a declared, approved, proven, smoked cell under the cap runs, with its timeout, and is reserved", () => {
|
|
389
|
+
const own = join(dir, "run.jsonl");
|
|
390
|
+
assert.deepEqual(
|
|
391
|
+
gate(CELL, own),
|
|
392
|
+
ok(
|
|
393
|
+
JSON.stringify({
|
|
394
|
+
allowed: true,
|
|
395
|
+
cell: { unit: "u1", arm: "control", model: "m1", seed: "1" },
|
|
396
|
+
attempt: "x0",
|
|
397
|
+
counted: true,
|
|
398
|
+
timeoutSec: 1800,
|
|
399
|
+
spent: 0,
|
|
400
|
+
}),
|
|
401
|
+
),
|
|
402
|
+
);
|
|
403
|
+
const [held] = records(own);
|
|
404
|
+
assert.deepEqual(held, {
|
|
405
|
+
attempt: "x0",
|
|
406
|
+
unit: "u1",
|
|
407
|
+
arm: "control",
|
|
408
|
+
model: "m1",
|
|
409
|
+
seed: "1",
|
|
410
|
+
status: "reserved",
|
|
411
|
+
cost: 2,
|
|
412
|
+
start: held?.start,
|
|
413
|
+
expires: new Date(
|
|
414
|
+
Date.parse(String(held?.start)) + 21_600_000,
|
|
415
|
+
).toISOString(),
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("gate: a smoke unit needs no approval and no smoke result", () => {
|
|
420
|
+
assert.deepEqual(
|
|
421
|
+
gate(
|
|
422
|
+
[
|
|
423
|
+
"--unit",
|
|
424
|
+
"u-smoke",
|
|
425
|
+
"--arm",
|
|
426
|
+
"treatment",
|
|
427
|
+
"--model",
|
|
428
|
+
"m2",
|
|
429
|
+
"--seed",
|
|
430
|
+
"2",
|
|
431
|
+
],
|
|
432
|
+
join(dir, "smoke.jsonl"),
|
|
433
|
+
),
|
|
434
|
+
ok(
|
|
435
|
+
JSON.stringify({
|
|
436
|
+
allowed: true,
|
|
437
|
+
cell: { unit: "u-smoke", arm: "treatment", model: "m2", seed: "2" },
|
|
438
|
+
attempt: "x0",
|
|
439
|
+
counted: false,
|
|
440
|
+
timeoutSec: null,
|
|
441
|
+
spent: 0,
|
|
442
|
+
}),
|
|
443
|
+
),
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("gate: everything wrong with a cell is named at once", () => {
|
|
448
|
+
const refused = (cell: string, why: string[]): ScriptResult =>
|
|
449
|
+
no(1, `refused ${cell}:\n - ${why.join("\n - ")}`);
|
|
450
|
+
assert.deepEqual(
|
|
451
|
+
gate(["--unit", "u9", "--arm", "x", "--model", "m9", "--seed", "7"]),
|
|
452
|
+
refused('{"unit":"u9","arm":"x","model":"m9","seed":"7"}', [
|
|
453
|
+
'unit "u9" is not in the registry',
|
|
454
|
+
'arm "x" is not declared',
|
|
455
|
+
'model "m9" is not declared',
|
|
456
|
+
'seed "7" is not declared',
|
|
457
|
+
]),
|
|
458
|
+
);
|
|
459
|
+
// Approval written by the agent, no sandbox proof, a model never smoked.
|
|
460
|
+
assert.deepEqual(
|
|
461
|
+
gate([
|
|
462
|
+
"--unit",
|
|
463
|
+
"u-agent",
|
|
464
|
+
"--arm",
|
|
465
|
+
"control",
|
|
466
|
+
"--model",
|
|
467
|
+
"m2",
|
|
468
|
+
"--seed",
|
|
469
|
+
"1",
|
|
470
|
+
]),
|
|
471
|
+
refused('{"unit":"u-agent","arm":"control","model":"m2","seed":"1"}', [
|
|
472
|
+
'unit "u-agent" has no approval by the author (approval.by = author, with at and channel)',
|
|
473
|
+
'unit "u-agent" has no sandbox proof (toolVisible, firesOnPinned, acceptsReference)',
|
|
474
|
+
'model "m2" has no smoke result (smoke.timeoutSec, smoke.costPerRun)',
|
|
475
|
+
]),
|
|
476
|
+
);
|
|
477
|
+
assert.deepEqual(
|
|
478
|
+
gate([
|
|
479
|
+
"--unit",
|
|
480
|
+
"u-out",
|
|
481
|
+
"--arm",
|
|
482
|
+
"control",
|
|
483
|
+
"--model",
|
|
484
|
+
"m1",
|
|
485
|
+
"--seed",
|
|
486
|
+
"1",
|
|
487
|
+
]),
|
|
488
|
+
refused('{"unit":"u-out","arm":"control","model":"m1","seed":"1"}', [
|
|
489
|
+
'unit "u-out" has role "excluded" (the pinned state does not build); only "counted" and "smoke" units run',
|
|
490
|
+
]),
|
|
491
|
+
);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("gate: a counted cell is skipped, not re-run; spend counts every attempt, against the cap", () => {
|
|
495
|
+
writeTree(dir, {
|
|
496
|
+
"attempts.jsonl": [
|
|
497
|
+
{
|
|
498
|
+
attempt: "a1",
|
|
499
|
+
unit: "u1",
|
|
500
|
+
arm: "control",
|
|
501
|
+
model: "m1",
|
|
502
|
+
seed: 1,
|
|
503
|
+
start: "t0",
|
|
504
|
+
},
|
|
505
|
+
{ attempt: "a1", end: "t1", status: "complete", cost: 3 },
|
|
506
|
+
{
|
|
507
|
+
attempt: "a2",
|
|
508
|
+
unit: "u1",
|
|
509
|
+
arm: "treatment",
|
|
510
|
+
model: "m1",
|
|
511
|
+
seed: 1,
|
|
512
|
+
cost: 4,
|
|
513
|
+
status: "harness-error",
|
|
514
|
+
},
|
|
515
|
+
// Crashed before it reported a cost: on record, costs nothing known.
|
|
516
|
+
{
|
|
517
|
+
attempt: "a3",
|
|
518
|
+
unit: "u-smoke",
|
|
519
|
+
arm: "control",
|
|
520
|
+
model: "m2",
|
|
521
|
+
seed: 2,
|
|
522
|
+
start: "t2",
|
|
523
|
+
},
|
|
524
|
+
"",
|
|
525
|
+
]
|
|
526
|
+
.map((r) => (r === "" ? "" : JSON.stringify(r)))
|
|
527
|
+
.join("\n"),
|
|
528
|
+
});
|
|
529
|
+
assert.deepEqual(
|
|
530
|
+
gate(CELL),
|
|
531
|
+
no(
|
|
532
|
+
3,
|
|
533
|
+
'{"unit":"u1","arm":"control","model":"m1","seed":"1"} is already counted; skip it',
|
|
534
|
+
),
|
|
535
|
+
);
|
|
536
|
+
// 3 + 4 spent, next 2 → 9, under 10: runs. A harness error is not counted, so a2 runs again.
|
|
537
|
+
assert.equal(
|
|
538
|
+
gate(["--unit", "u1", "--arm", "treatment", "--model", "m1", "--seed", "1"])
|
|
539
|
+
.status,
|
|
540
|
+
0,
|
|
541
|
+
);
|
|
542
|
+
writeTree(dir, {
|
|
543
|
+
"more.jsonl": `${JSON.stringify({ attempt: "b", cost: 9 })}\n`,
|
|
544
|
+
});
|
|
545
|
+
assert.deepEqual(
|
|
546
|
+
gate(CELL, join(dir, "more.jsonl")),
|
|
547
|
+
no(
|
|
548
|
+
1,
|
|
549
|
+
'refused {"unit":"u1","arm":"control","model":"m1","seed":"1"}:\n - budget: spent 9 + next 2 exceeds the cap 10',
|
|
550
|
+
),
|
|
551
|
+
);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("gate: only the COMMITTED registry allows anything", () => {
|
|
555
|
+
const d = repo("gate-dirty", { "grid.json": JSON.stringify(REGISTRY) });
|
|
556
|
+
writeTree(d, {
|
|
557
|
+
"grid.json": JSON.stringify({
|
|
558
|
+
...REGISTRY,
|
|
559
|
+
models: [...REGISTRY.models, { id: "m3" }],
|
|
560
|
+
}),
|
|
561
|
+
});
|
|
562
|
+
const r = guard([
|
|
563
|
+
"gate",
|
|
564
|
+
"--registry",
|
|
565
|
+
join(d, "grid.json"),
|
|
566
|
+
"--ledger",
|
|
567
|
+
ledger,
|
|
568
|
+
...CELL,
|
|
569
|
+
"--attempt",
|
|
570
|
+
"x0",
|
|
571
|
+
]);
|
|
572
|
+
assert.deepEqual(
|
|
573
|
+
r,
|
|
574
|
+
no(
|
|
575
|
+
1,
|
|
576
|
+
`${join(d, "grid.json")} differs from its committed version; commit the change (a dated addendum) before any run it allows`,
|
|
577
|
+
),
|
|
578
|
+
);
|
|
579
|
+
writeTree(d, { "new.json": "{}" });
|
|
580
|
+
assert.deepEqual(
|
|
581
|
+
guard([
|
|
582
|
+
"gate",
|
|
583
|
+
"--registry",
|
|
584
|
+
join(d, "new.json"),
|
|
585
|
+
"--ledger",
|
|
586
|
+
ledger,
|
|
587
|
+
...CELL,
|
|
588
|
+
"--attempt",
|
|
589
|
+
"x0",
|
|
590
|
+
]),
|
|
591
|
+
no(
|
|
592
|
+
1,
|
|
593
|
+
`${join(d, "new.json")} is not committed; commit it before any run it allows`,
|
|
594
|
+
),
|
|
595
|
+
);
|
|
596
|
+
const loose = writeTree(join(WORK, "no-git"), { "grid.json": "{}" });
|
|
597
|
+
// WORK is a temp directory outside any repository, so git finds none above it either.
|
|
598
|
+
assert.deepEqual(
|
|
599
|
+
guard([
|
|
600
|
+
"gate",
|
|
601
|
+
"--registry",
|
|
602
|
+
join(loose, "grid.json"),
|
|
603
|
+
"--ledger",
|
|
604
|
+
ledger,
|
|
605
|
+
...CELL,
|
|
606
|
+
"--attempt",
|
|
607
|
+
"x0",
|
|
608
|
+
]),
|
|
609
|
+
no(
|
|
610
|
+
1,
|
|
611
|
+
`${join(loose, "grid.json")} is not inside a git repository; the registry must be committed`,
|
|
612
|
+
),
|
|
613
|
+
);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("gate: an empty registry: no budget, no draw, an excluded unit without a reason", () => {
|
|
617
|
+
const d = repo("gate-empty", {
|
|
618
|
+
"grid.json": JSON.stringify({
|
|
619
|
+
units: [
|
|
620
|
+
{ id: "u1", role: "counted" },
|
|
621
|
+
{ id: "x", role: "excluded" },
|
|
622
|
+
],
|
|
623
|
+
}),
|
|
624
|
+
});
|
|
625
|
+
assert.deepEqual(
|
|
626
|
+
guard([
|
|
627
|
+
"gate",
|
|
628
|
+
"--registry",
|
|
629
|
+
join(d, "grid.json"),
|
|
630
|
+
"--ledger",
|
|
631
|
+
ledger,
|
|
632
|
+
...CELL,
|
|
633
|
+
"--attempt",
|
|
634
|
+
"x0",
|
|
635
|
+
]),
|
|
636
|
+
no(
|
|
637
|
+
1,
|
|
638
|
+
[
|
|
639
|
+
'refused {"unit":"u1","arm":"control","model":"m1","seed":"1"}:',
|
|
640
|
+
'arm "control" is not declared',
|
|
641
|
+
'model "m1" is not declared',
|
|
642
|
+
'seed "1" is not declared',
|
|
643
|
+
"an excluded unit has no reason",
|
|
644
|
+
"the registry declares no budget.cap",
|
|
645
|
+
'unit "u1" has no approval by the author (approval.by = author, with at and channel)',
|
|
646
|
+
'unit "u1" has no sandbox proof (toolVisible, firesOnPinned, acceptsReference)',
|
|
647
|
+
"the registry records no draw seed",
|
|
648
|
+
].join("\n - "),
|
|
649
|
+
),
|
|
650
|
+
);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test("gate: a reserved cell is held until its attempt ends or the reservation lapses", () => {
|
|
654
|
+
const own = join(dir, "held.jsonl");
|
|
655
|
+
assert.equal(gate([...CELL, "--attempt", "r1"], own).status, 0);
|
|
656
|
+
const again = gate([...CELL, "--attempt", "r2"], own);
|
|
657
|
+
const [held] = records(own);
|
|
658
|
+
assert.deepEqual(
|
|
659
|
+
again,
|
|
660
|
+
no(
|
|
661
|
+
3,
|
|
662
|
+
`{"unit":"u1","arm":"control","model":"m1","seed":"1"} is reserved by attempt r1 until ${String(held?.expires)}; skip it`,
|
|
663
|
+
),
|
|
664
|
+
);
|
|
665
|
+
// The driver's end record for r1, a harness error: the cell is free again, at its real cost.
|
|
666
|
+
writeTree(dir, {
|
|
667
|
+
"held.jsonl": `${readFileSync(own, "utf8")}${JSON.stringify({ attempt: "r1", status: "harness-error", cost: 1 })}\n`,
|
|
668
|
+
});
|
|
669
|
+
const r3 = gate([...CELL, "--attempt", "r3"], own);
|
|
670
|
+
assert.equal(r3.status, 0, r3.stderr);
|
|
671
|
+
assert.match(r3.stdout, /"spent":1\}/);
|
|
672
|
+
// An attempt id is used once.
|
|
673
|
+
assert.deepEqual(
|
|
674
|
+
gate(
|
|
675
|
+
[
|
|
676
|
+
"--unit",
|
|
677
|
+
"u-smoke",
|
|
678
|
+
"--arm",
|
|
679
|
+
"control",
|
|
680
|
+
"--model",
|
|
681
|
+
"m2",
|
|
682
|
+
"--seed",
|
|
683
|
+
"1",
|
|
684
|
+
"--attempt",
|
|
685
|
+
"r3",
|
|
686
|
+
],
|
|
687
|
+
own,
|
|
688
|
+
),
|
|
689
|
+
no(1, "attempt r3 is already on the ledger; give each attempt its own id"),
|
|
690
|
+
);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test("gate: a lapsed reservation neither holds the cell nor counts as spent", () => {
|
|
694
|
+
const own = join(dir, "lapsed.jsonl");
|
|
695
|
+
assert.equal(
|
|
696
|
+
gate([...CELL, "--attempt", "l1", "--reserve-sec", "0"], own).status,
|
|
697
|
+
0,
|
|
698
|
+
);
|
|
699
|
+
const r = gate([...CELL, "--attempt", "l2"], own);
|
|
700
|
+
assert.equal(r.status, 0, r.stderr);
|
|
701
|
+
assert.match(r.stdout, /"spent":0\}/);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
/** `gate` as a child process that runs while others do; resolves to its exit code. */
|
|
705
|
+
const gateAsync = (
|
|
706
|
+
cell: readonly string[],
|
|
707
|
+
ledgerPath: string,
|
|
708
|
+
): Promise<number | null> =>
|
|
709
|
+
new Promise((done) => {
|
|
710
|
+
spawn(
|
|
711
|
+
process.execPath,
|
|
712
|
+
[SCRIPT, "gate", "--registry", reg, "--ledger", ledgerPath, ...cell],
|
|
713
|
+
{
|
|
714
|
+
cwd: WORK,
|
|
715
|
+
env: { ...process.env, ...GIT_ENV },
|
|
716
|
+
stdio: "ignore",
|
|
717
|
+
},
|
|
718
|
+
).on("exit", done);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test("gate: two drivers asking at once for one cell — exactly one gets it", async () => {
|
|
722
|
+
const own = join(dir, "race-cell.jsonl");
|
|
723
|
+
const codes = await Promise.all(
|
|
724
|
+
["c1", "c2", "c3", "c4"].map((id) =>
|
|
725
|
+
gateAsync([...CELL, "--attempt", id], own),
|
|
726
|
+
),
|
|
727
|
+
);
|
|
728
|
+
assert.deepEqual([...codes].sort(), [0, 3, 3, 3]);
|
|
729
|
+
assert.equal(records(own).length, 1);
|
|
730
|
+
}, 60_000);
|
|
731
|
+
|
|
732
|
+
test("gate: two drivers asking at once with room for one run under the cap — one is refused", async () => {
|
|
733
|
+
const own = join(dir, "race-budget.jsonl");
|
|
734
|
+
writeTree(dir, {
|
|
735
|
+
"race-budget.jsonl": `${JSON.stringify({ attempt: "old", cost: 7 })}\n`,
|
|
736
|
+
});
|
|
737
|
+
const treatment = [
|
|
738
|
+
"--unit",
|
|
739
|
+
"u1",
|
|
740
|
+
"--arm",
|
|
741
|
+
"treatment",
|
|
742
|
+
"--model",
|
|
743
|
+
"m1",
|
|
744
|
+
"--seed",
|
|
745
|
+
"1",
|
|
746
|
+
];
|
|
747
|
+
const codes = await Promise.all([
|
|
748
|
+
gateAsync([...CELL, "--attempt", "b1"], own),
|
|
749
|
+
gateAsync([...treatment, "--attempt", "b2"], own),
|
|
750
|
+
]);
|
|
751
|
+
// 7 spent + 2 reserved = 9, then 9 + 2 = 11 > 10.
|
|
752
|
+
assert.deepEqual([...codes].sort(), [0, 1]);
|
|
753
|
+
}, 60_000);
|
|
754
|
+
|
|
755
|
+
test("gate: a ledger lock held by a live gate is waited for, then refused; a dead holder's is taken over", () => {
|
|
756
|
+
const own = join(dir, "locked.jsonl");
|
|
757
|
+
const hold = (pid: number): void => {
|
|
758
|
+
writeTree(dir, {
|
|
759
|
+
"locked.jsonl.lock": JSON.stringify({
|
|
760
|
+
owner: "gate",
|
|
761
|
+
pid,
|
|
762
|
+
host: hostname(),
|
|
763
|
+
at: "t",
|
|
764
|
+
}),
|
|
765
|
+
});
|
|
766
|
+
};
|
|
767
|
+
hold(process.pid);
|
|
768
|
+
const r = runNode(
|
|
769
|
+
SCRIPT,
|
|
770
|
+
["gate", "--registry", reg, "--ledger", own, ...CELL, "--attempt", "k1"],
|
|
771
|
+
{ cwd: WORK, env: { ...GIT_ENV, GRID_GUARD_LEDGER_WAIT_MS: "100" } },
|
|
772
|
+
);
|
|
773
|
+
assert.deepEqual(
|
|
774
|
+
r,
|
|
775
|
+
no(
|
|
776
|
+
1,
|
|
777
|
+
`${own}.lock is held by pid ${String(process.pid)} on ${hostname()} since t; another gate is writing the ledger — try again`,
|
|
778
|
+
),
|
|
779
|
+
);
|
|
780
|
+
// A lock file another gate has created and not yet written: held, and said so when time is up.
|
|
781
|
+
writeTree(dir, { "locked.jsonl.lock": "" });
|
|
782
|
+
assert.deepEqual(
|
|
783
|
+
runNode(
|
|
784
|
+
SCRIPT,
|
|
785
|
+
["gate", "--registry", reg, "--ledger", own, ...CELL, "--attempt", "k1"],
|
|
786
|
+
{ cwd: WORK, env: { ...GIT_ENV, GRID_GUARD_LEDGER_WAIT_MS: "100" } },
|
|
787
|
+
),
|
|
788
|
+
no(
|
|
789
|
+
1,
|
|
790
|
+
`${own}.lock exists and names no owner; if no gate is running, delete it`,
|
|
791
|
+
),
|
|
792
|
+
);
|
|
793
|
+
hold(spawnSync(process.execPath, ["-e", ""]).pid);
|
|
794
|
+
const taken = gate([...CELL, "--attempt", "k2"], own);
|
|
795
|
+
assert.equal(taken.status, 0, taken.stderr);
|
|
796
|
+
assert.equal(existsSync(`${own}.lock`), false);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
const ledgerFile = (name: string, recs: readonly object[]): string =>
|
|
800
|
+
join(
|
|
801
|
+
writeTree(WORK, {
|
|
802
|
+
[name]: recs.map((r) => JSON.stringify(r)).join("\n"),
|
|
803
|
+
}),
|
|
804
|
+
name,
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
test("overlap: sequential attempts on one root, and parallel ones on two roots, are clean", () => {
|
|
808
|
+
const f = ledgerFile("clean.jsonl", [
|
|
809
|
+
{
|
|
810
|
+
attempt: "a",
|
|
811
|
+
root: "/w/1",
|
|
812
|
+
start: "2026-01-01T00:00:00Z",
|
|
813
|
+
end: "2026-01-01T01:00:00Z",
|
|
814
|
+
status: "complete",
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
attempt: "b",
|
|
818
|
+
root: "/w/1",
|
|
819
|
+
start: "2026-01-01T01:00:00Z",
|
|
820
|
+
end: "2026-01-01T02:00:00Z",
|
|
821
|
+
status: "complete",
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
attempt: "c",
|
|
825
|
+
root: "/w/2",
|
|
826
|
+
start: "2026-01-01T00:30:00Z",
|
|
827
|
+
end: "2026-01-01T01:30:00Z",
|
|
828
|
+
status: "complete",
|
|
829
|
+
},
|
|
830
|
+
// Two harness errors overlapping: already marked, nothing counted is at risk.
|
|
831
|
+
{
|
|
832
|
+
attempt: "d",
|
|
833
|
+
root: "/w/3",
|
|
834
|
+
start: "2026-01-01T00:00:00Z",
|
|
835
|
+
end: "2026-01-01T02:00:00Z",
|
|
836
|
+
status: "harness-error",
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
attempt: "e",
|
|
840
|
+
root: "/w/3",
|
|
841
|
+
start: "2026-01-01T01:00:00Z",
|
|
842
|
+
end: "2026-01-01T03:00:00Z",
|
|
843
|
+
status: "harness-error",
|
|
844
|
+
},
|
|
845
|
+
]);
|
|
846
|
+
assert.deepEqual(guard(["overlap", f]), ok("no overlap among 5 attempts"));
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
test("overlap: two attempts on one root at once are flagged, an unfinished one overlaps everything after it", () => {
|
|
850
|
+
const f = ledgerFile("dirty.jsonl", [
|
|
851
|
+
{
|
|
852
|
+
attempt: "a",
|
|
853
|
+
root: "/w/1",
|
|
854
|
+
start: "2026-01-01T00:00:00Z",
|
|
855
|
+
end: "2026-01-01T02:00:00Z",
|
|
856
|
+
status: "complete",
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
attempt: "b",
|
|
860
|
+
root: "/w/1/",
|
|
861
|
+
start: "2026-01-01T01:00:00Z",
|
|
862
|
+
end: "2026-01-01T03:00:00Z",
|
|
863
|
+
status: "complete",
|
|
864
|
+
},
|
|
865
|
+
{ attempt: "c", root: "/w/2", start: "2026-01-01T00:00:00Z" },
|
|
866
|
+
{
|
|
867
|
+
attempt: "d",
|
|
868
|
+
root: "/w/2",
|
|
869
|
+
start: "2026-06-01T00:00:00Z",
|
|
870
|
+
end: "2026-06-01T01:00:00Z",
|
|
871
|
+
status: "complete",
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
attempt: "e",
|
|
875
|
+
root: "/w/9",
|
|
876
|
+
start: "yesterday",
|
|
877
|
+
end: "2026-01-01T00:00:00Z",
|
|
878
|
+
},
|
|
879
|
+
]);
|
|
880
|
+
assert.deepEqual(
|
|
881
|
+
guard(["overlap", f]),
|
|
882
|
+
no(
|
|
883
|
+
1,
|
|
884
|
+
[
|
|
885
|
+
"e: start or end is not a date",
|
|
886
|
+
"a and b shared /w/1",
|
|
887
|
+
"c and d shared /w/2",
|
|
888
|
+
'mark each contaminated attempt "harness-error" (keep it) and run the cell again',
|
|
889
|
+
].join("\n"),
|
|
890
|
+
),
|
|
891
|
+
);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
test("overlap: a missing ledger is an error, not a clean audit", () => {
|
|
895
|
+
const f = join(WORK, "absent.jsonl");
|
|
896
|
+
assert.deepEqual(guard(["overlap", f]), no(1, `no ledger at ${f}`));
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
/** A hand-built environment: nothing inherited but what node, git and coverage need. */
|
|
900
|
+
const probe = (
|
|
901
|
+
args: readonly string[],
|
|
902
|
+
env: Readonly<Record<string, string>>,
|
|
903
|
+
): ScriptResult => {
|
|
904
|
+
const keep = ["PATH", "HOME", "NODE_V8_COVERAGE", "NODE_OPTIONS"].filter(
|
|
905
|
+
(k) => process.env[k] !== undefined,
|
|
906
|
+
);
|
|
907
|
+
const r = spawnSync(process.execPath, [SCRIPT, "env-probe", ...args], {
|
|
908
|
+
encoding: "utf8",
|
|
909
|
+
env: {
|
|
910
|
+
...Object.fromEntries(keep.map((k) => [k, process.env[k]])),
|
|
911
|
+
...GIT_ENV,
|
|
912
|
+
...env,
|
|
913
|
+
},
|
|
914
|
+
});
|
|
915
|
+
return { status: r.status, stdout: r.stdout, stderr: r.stderr };
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
test("env-probe: clean, and git commits", () => {
|
|
919
|
+
assert.deepEqual(probe([], {}), ok("no secret-named variables"));
|
|
920
|
+
assert.deepEqual(
|
|
921
|
+
probe(["--git"], {}),
|
|
922
|
+
ok("no secret-named variables; git init, add and commit work"),
|
|
923
|
+
);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
test("env-probe: secret-named variables are named, their values never printed; --allow accepts one by name", () => {
|
|
927
|
+
const value = "do-not-print-this-value-123456";
|
|
928
|
+
const r = probe([], {
|
|
929
|
+
SERVICE_TOKEN: value,
|
|
930
|
+
DB_PASSWORD: value,
|
|
931
|
+
PLAIN: value,
|
|
932
|
+
});
|
|
933
|
+
assert.deepEqual(
|
|
934
|
+
r,
|
|
935
|
+
no(
|
|
936
|
+
1,
|
|
937
|
+
"secret-named variables reach this environment (names only): DB_PASSWORD, SERVICE_TOKEN",
|
|
938
|
+
),
|
|
939
|
+
);
|
|
940
|
+
assert.deepEqual(
|
|
941
|
+
probe(["--allow", "SERVICE_TOKEN", "--allow", "DB_PASSWORD"], {
|
|
942
|
+
SERVICE_TOKEN: value,
|
|
943
|
+
DB_PASSWORD: value,
|
|
944
|
+
}),
|
|
945
|
+
ok("no secret-named variables"),
|
|
946
|
+
);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
test("env-probe: a policy that broke git is caught", () => {
|
|
950
|
+
// Git told to read its configuration from a file that cannot parse: the shape of a sandbox
|
|
951
|
+
// whose environment policy removed or mangled what git needs.
|
|
952
|
+
const bad = writeTree(join(WORK, "badcfg"), { gitconfig: "[[[\n" });
|
|
953
|
+
const r = probe(["--git"], { GIT_CONFIG_GLOBAL: join(bad, "gitconfig") });
|
|
954
|
+
assert.equal(r.status, 1);
|
|
955
|
+
assert.equal(r.stdout, "");
|
|
956
|
+
assert.match(
|
|
957
|
+
r.stderr,
|
|
958
|
+
/^grid-guard: git init fails in this environment: .*bad config/s,
|
|
959
|
+
);
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
test("env-probe: git missing altogether is caught", () => {
|
|
963
|
+
const r = probe(["--git"], { PATH: "/nonexistent" });
|
|
964
|
+
assert.deepEqual(
|
|
965
|
+
r,
|
|
966
|
+
no(1, "git init fails in this environment: Error: spawnSync git ENOENT"),
|
|
967
|
+
);
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
// Planted values, assembled at run time so that this file itself carries none of them.
|
|
971
|
+
const FAKE = {
|
|
972
|
+
aws: `AKIA${"Q".repeat(16)}`,
|
|
973
|
+
sk: `sk-${"a1".repeat(16)}`,
|
|
974
|
+
dump: `SERVICE_TOKEN=${"z9".repeat(10)}`,
|
|
975
|
+
pem: ["-----BEGIN RSA", "PRIVATE", "KEY-----"].join(" "),
|
|
976
|
+
gh: `ghp_${"b".repeat(36)}`,
|
|
977
|
+
chat: `xoxb-${"1".repeat(12)}`,
|
|
978
|
+
cloud: `AIza${"c".repeat(35)}`,
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
test("scan-keys: clean files, placeholders, binaries and skipped folders stay silent", () => {
|
|
982
|
+
const d = writeTree(join(WORK, "clean-art"), {
|
|
983
|
+
"README.md":
|
|
984
|
+
"Set API_KEY=<your key here> or API_KEY=$FROM_ENV before running.\n",
|
|
985
|
+
"data/results.csv": "unit,arm,outcome\nu1,control,pass\n",
|
|
986
|
+
"blob.bin": `\0${FAKE.aws}`,
|
|
987
|
+
"node_modules/x/leak.txt": FAKE.aws,
|
|
988
|
+
".git/leak.txt": FAKE.aws,
|
|
989
|
+
});
|
|
990
|
+
assert.deepEqual(
|
|
991
|
+
guard(["scan-keys", d]),
|
|
992
|
+
ok("no key-shaped strings in 3 files"),
|
|
993
|
+
);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
test("scan-keys: every planted shape fires, by file and line, and no value is printed", () => {
|
|
997
|
+
const d = writeTree(join(WORK, "dirty-art"), {
|
|
998
|
+
"transcript.log": ["ok", FAKE.dump, FAKE.aws, FAKE.sk].join("\n"),
|
|
999
|
+
"keys/id": [FAKE.pem, FAKE.gh, FAKE.chat, FAKE.cloud].join("\n"),
|
|
1000
|
+
});
|
|
1001
|
+
const r = guard(["scan-keys", join(d, "transcript.log"), join(d, "keys")]);
|
|
1002
|
+
assert.deepEqual(
|
|
1003
|
+
r,
|
|
1004
|
+
no(
|
|
1005
|
+
1,
|
|
1006
|
+
[
|
|
1007
|
+
"key-shaped strings (values not shown):",
|
|
1008
|
+
`${d}/transcript.log:2: secret-named assignment`,
|
|
1009
|
+
`${d}/transcript.log:3: access key id`,
|
|
1010
|
+
`${d}/transcript.log:4: sk- api key`,
|
|
1011
|
+
`${d}/keys/id:1: private key block`,
|
|
1012
|
+
`${d}/keys/id:2: git host token`,
|
|
1013
|
+
`${d}/keys/id:3: chat bot token`,
|
|
1014
|
+
`${d}/keys/id:4: cloud api key`,
|
|
1015
|
+
].join("\n"),
|
|
1016
|
+
),
|
|
1017
|
+
);
|
|
1018
|
+
for (const v of Object.values(FAKE)) assert.ok(!r.stderr.includes(v));
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test("precommit: a clean staged change passes; a staged fake key is refused without its value", () => {
|
|
1022
|
+
const d = repo("pc", { "README.md": "x\n" });
|
|
1023
|
+
writeTree(d, { "notes.md": "fine\n" });
|
|
1024
|
+
git(d, "add", "notes.md");
|
|
1025
|
+
assert.deepEqual(
|
|
1026
|
+
guard(["precommit"], d),
|
|
1027
|
+
ok("no key-shaped strings in 1 files"),
|
|
1028
|
+
);
|
|
1029
|
+
writeTree(d, { "run.log": `env dump\n${FAKE.dump}\n` });
|
|
1030
|
+
git(d, "add", "run.log");
|
|
1031
|
+
// The working copy is cleaned, the INDEX still holds the key: the index is what commits.
|
|
1032
|
+
writeTree(d, { "run.log": "env dump\n" });
|
|
1033
|
+
const r = guard(["precommit"], d);
|
|
1034
|
+
assert.deepEqual(
|
|
1035
|
+
r,
|
|
1036
|
+
no(
|
|
1037
|
+
1,
|
|
1038
|
+
"key-shaped strings (values not shown):\nrun.log:2: secret-named assignment",
|
|
1039
|
+
),
|
|
1040
|
+
);
|
|
1041
|
+
assert.ok(!r.stderr.includes(FAKE.dump.split("=")[1] ?? "?"));
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
test("precommit: a staged run folder without its completion record is refused; a finished one passes", () => {
|
|
1045
|
+
const d = repo("pc-runs", { "README.md": "x\n" });
|
|
1046
|
+
writeTree(d, {
|
|
1047
|
+
"runs/c1/transcript.txt": "t\n",
|
|
1048
|
+
"runs/c1/done.json": "{}\n",
|
|
1049
|
+
"runs/c2/transcript.txt": "half\n",
|
|
1050
|
+
"runs/index.md": "list\n",
|
|
1051
|
+
});
|
|
1052
|
+
git(d, "add", "runs");
|
|
1053
|
+
assert.deepEqual(
|
|
1054
|
+
guard(["precommit", "--runs", "runs/"], d),
|
|
1055
|
+
no(
|
|
1056
|
+
1,
|
|
1057
|
+
"run folders without done.json are staged (commit only finished runs):\nruns/c2",
|
|
1058
|
+
),
|
|
1059
|
+
);
|
|
1060
|
+
git(d, "rm", "-q", "--cached", "runs/c2/transcript.txt");
|
|
1061
|
+
assert.deepEqual(
|
|
1062
|
+
guard(["precommit", "--runs", "runs"], d),
|
|
1063
|
+
ok("no key-shaped strings in 3 files"),
|
|
1064
|
+
);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
test("precommit: outside a repository it says so", () => {
|
|
1068
|
+
const d = writeTree(join(WORK, "pc-none"), { "x.txt": "x" });
|
|
1069
|
+
const r = guard(["precommit"], d);
|
|
1070
|
+
assert.equal(r.status, 1);
|
|
1071
|
+
assert.match(
|
|
1072
|
+
r.stderr,
|
|
1073
|
+
/^grid-guard: git rev-parse: fatal: not a git repository/,
|
|
1074
|
+
);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
test("precommit: a staged deletion of a run's completion record is refused", () => {
|
|
1078
|
+
const d = repo("pc-delete", {
|
|
1079
|
+
"runs/c1/transcript.txt": "t\n",
|
|
1080
|
+
"runs/c1/done.json": "{}\n",
|
|
1081
|
+
});
|
|
1082
|
+
git(d, "rm", "-q", "--cached", "runs/c1/done.json");
|
|
1083
|
+
assert.deepEqual(
|
|
1084
|
+
guard(["precommit", "--runs", "runs"], d),
|
|
1085
|
+
no(
|
|
1086
|
+
1,
|
|
1087
|
+
"run folders without done.json are staged (commit only finished runs):\nruns/c1",
|
|
1088
|
+
),
|
|
1089
|
+
);
|
|
1090
|
+
});
|