independent-tests 0.1.0 → 0.3.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 +12 -8
- package/README.md +133 -65
- package/RESULTS.md +119 -8
- package/SKILL.md +23 -13
- package/bin/backends.mjs +55 -25
- package/bin/generate.mjs +3 -3
- package/bin/handout.mjs +3 -3
- package/bin/lib.mjs +278 -86
- package/bin/platform.mjs +145 -0
- package/bin/run.mjs +6 -4
- package/bin/runners/boundary.mjs +27 -8
- package/bin/runners/guard.mjs +8 -6
- package/bin/runners/native-build.mjs +63 -0
- package/bin/runners/pytest.mjs +3 -2
- package/bin/runners/shared.mjs +10 -7
- package/package.json +8 -7
- package/prompts/agent.md +1 -1
- package/prompts/exec/agent.md +1 -1
- package/prompts/exec-native/agent.md +29 -0
- package/prompts/exec-native/hidden-tests.md +66 -0
- package/prompts/exec-native/public-tests.md +42 -0
- package/prompts/exec-native/reference.md +19 -0
- package/prompts/exec-native/skeleton.md +24 -0
- package/prompts/exec-native/spec-from-ticket.md +29 -0
- package/prompts/http/agent.md +1 -1
- package/prompts/http-native/agent.md +31 -0
- package/prompts/http-native/hidden-tests.md +70 -0
- package/prompts/http-native/public-tests.md +43 -0
- package/prompts/http-native/reference.md +20 -0
- package/prompts/http-native/skeleton.md +26 -0
- package/prompts/http-native/spec-from-ticket.md +30 -0
- package/template/package-lock.json +1 -1
- package/template/package.json +2 -2
- package/template/sandbox/run.mjs +3 -2
- package/template/sandbox/test.mjs +30 -0
- package/template-python/README.md +2 -1
- package/template-python/sandbox/run.py +5 -3
package/bin/backends.mjs
CHANGED
|
@@ -45,8 +45,12 @@
|
|
|
45
45
|
//
|
|
46
46
|
// `placement`'s denial is checked by evidence/backends.mjs `sandbox`, a
|
|
47
47
|
// negative test that reads a hidden-suite path and the home from inside.
|
|
48
|
+
// Windows has neither sandbox: there `placement` runs the command as it
|
|
49
|
+
// is, the shell being Git for Windows' bash, and says so once (lib.mjs
|
|
50
|
+
// `warnUnsandboxed`); the staged workdir and the transcript audit are what
|
|
51
|
+
// keep a writer blind, and anthropic-api's file tools stay confined.
|
|
48
52
|
|
|
49
|
-
import { spawn
|
|
53
|
+
import { spawn } from "node:child_process";
|
|
50
54
|
import { randomUUID } from "node:crypto";
|
|
51
55
|
import {
|
|
52
56
|
appendFileSync,
|
|
@@ -61,7 +65,7 @@ import {
|
|
|
61
65
|
} from "node:fs";
|
|
62
66
|
import { createRequire } from "node:module";
|
|
63
67
|
import { homedir, tmpdir } from "node:os";
|
|
64
|
-
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
68
|
+
import { basename, dirname, join, posix, relative, resolve } from "node:path";
|
|
65
69
|
import { pathToFileURL } from "node:url";
|
|
66
70
|
import {
|
|
67
71
|
DEFAULT_MODEL,
|
|
@@ -78,14 +82,15 @@ import {
|
|
|
78
82
|
log,
|
|
79
83
|
sha256,
|
|
80
84
|
takeSlot,
|
|
85
|
+
warnUnsandboxed,
|
|
81
86
|
} from "./lib.mjs";
|
|
87
|
+
import { GROUP, OS_SANDBOX, WINDOWS, gitBash, inside, pathKey, slash, spawnCommand, which } from "./platform.mjs";
|
|
82
88
|
|
|
83
89
|
export const CODEX_VERSION = "0.164.0";
|
|
84
90
|
export const SDK_VERSION = "0.137.0";
|
|
85
91
|
/** Per session: CODEX_HOME, HOME, TMPDIR, the API backend's messages. */
|
|
86
92
|
export const SESSIONS = join(WORK_ROOT, "sessions");
|
|
87
93
|
|
|
88
|
-
const inside = (path, folder) => path === folder || path.startsWith(`${folder}/`);
|
|
89
94
|
/** `path` made real as far as it exists (the sandboxes match real paths). */
|
|
90
95
|
export function real(path) {
|
|
91
96
|
let at = resolve(path);
|
|
@@ -110,7 +115,7 @@ export function installTool(name, spec, version) {
|
|
|
110
115
|
mkdirSync(partial, { recursive: true });
|
|
111
116
|
writeFileSync(join(partial, "package.json"), `${JSON.stringify({ private: true })}\n`);
|
|
112
117
|
log(`installing ${spec}@${version} into ${dest} (npm@10.9.2, once)`);
|
|
113
|
-
const out =
|
|
118
|
+
const out = spawnCommand("npx", ["-y", "npm@10.9.2", "install", "--no-audit", "--no-fund", "--save-exact", `${spec}@${version}`], {
|
|
114
119
|
cwd: partial,
|
|
115
120
|
encoding: "utf8",
|
|
116
121
|
maxBuffer: 64 * 1024 * 1024,
|
|
@@ -131,8 +136,8 @@ export function installTool(name, spec, version) {
|
|
|
131
136
|
|
|
132
137
|
/** The install prefix of a binary (two folders above its real path), for the sandbox's read list. */
|
|
133
138
|
function prefixOf(bin) {
|
|
134
|
-
const found =
|
|
135
|
-
if (found ===
|
|
139
|
+
const found = which(bin);
|
|
140
|
+
if (found === null) return null;
|
|
136
141
|
return dirname(dirname(realpathSync(found)));
|
|
137
142
|
}
|
|
138
143
|
|
|
@@ -203,16 +208,28 @@ export function bwrapArgs({ deny, readable, writable }, network, cwd) {
|
|
|
203
208
|
return out;
|
|
204
209
|
}
|
|
205
210
|
|
|
206
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* The shell `placement` runs a command line in: /bin/sh and /bin/bash on
|
|
213
|
+
* macOS and Linux, Git for Windows' bash on Windows (what Claude Code's
|
|
214
|
+
* Bash tool runs there).
|
|
215
|
+
*/
|
|
216
|
+
export function shellBin(name) {
|
|
217
|
+
if (!WINDOWS) return `/bin/${name}`;
|
|
218
|
+
const bash = gitBash();
|
|
219
|
+
if (bash === null) throw new Error("No bash: install Git for Windows, or set CLAUDE_CODE_GIT_BASH_PATH to its bash.exe.");
|
|
220
|
+
return bash;
|
|
221
|
+
}
|
|
222
|
+
/** `bin argv` wrapped in the platform's sandbox: [bin, argv]. Windows has none: [bin, argv] as given. */
|
|
207
223
|
export function placement(bin, argv, { cwd, state, read = [], network = true }) {
|
|
224
|
+
if (!OS_SANDBOX) {
|
|
225
|
+
warnUnsandboxed();
|
|
226
|
+
return [bin, argv];
|
|
227
|
+
}
|
|
208
228
|
const paths = placementPaths({ cwd, state, read });
|
|
209
229
|
if (process.platform === "darwin") return ["/usr/bin/sandbox-exec", ["-p", seatbeltProfile(paths, network), bin, ...argv]];
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return [found, [...bwrapArgs(paths, network, cwd), "--", bin, ...argv]];
|
|
214
|
-
}
|
|
215
|
-
throw new Error(`No sandbox for ${process.platform}: non-Claude backends run on macOS or Linux.`);
|
|
230
|
+
const found = which("bwrap");
|
|
231
|
+
if (found === null) throw new Error("No bwrap on PATH: a non-Claude backend on Linux needs bubblewrap for its sandbox.");
|
|
232
|
+
return [found, [...bwrapArgs(paths, network, cwd), "--", bin, ...argv]];
|
|
216
233
|
}
|
|
217
234
|
|
|
218
235
|
/** A session's state folder, kept across a resume of the same transcript and cleared on a fresh start. */
|
|
@@ -223,13 +240,25 @@ function stateOf(transcript, resume) {
|
|
|
223
240
|
return dir;
|
|
224
241
|
}
|
|
225
242
|
|
|
226
|
-
/**
|
|
243
|
+
/**
|
|
244
|
+
* The environment a placed process starts with: nothing of the caller's
|
|
245
|
+
* but the basics, plus on Windows what any process there needs to start
|
|
246
|
+
* (SystemRoot, ComSpec, PATHEXT) and the state folder as its profile and
|
|
247
|
+
* temp folder too.
|
|
248
|
+
*/
|
|
227
249
|
function childEnv(state, extra = {}) {
|
|
228
|
-
const keep = ["
|
|
250
|
+
const keep = ["LANG", "LC_ALL", "LC_CTYPE", "TERM", "USER", "LOGNAME", "SHELL"];
|
|
251
|
+
const windows = ["SystemRoot", "SystemDrive", "windir", "ComSpec", "PATHEXT", "USERNAME", "NUMBER_OF_PROCESSORS", "PROCESSOR_ARCHITECTURE"];
|
|
252
|
+
const home = join(state, "home");
|
|
253
|
+
const temp = join(state, "tmp");
|
|
229
254
|
return {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
255
|
+
[pathKey(process.env)]: process.env[pathKey(process.env)],
|
|
256
|
+
...Object.fromEntries(
|
|
257
|
+
[...keep, ...(WINDOWS ? windows : [])].filter((key) => process.env[key] !== undefined).map((key) => [key, process.env[key]]),
|
|
258
|
+
),
|
|
259
|
+
HOME: home,
|
|
260
|
+
TMPDIR: temp,
|
|
261
|
+
...(WINDOWS ? { USERPROFILE: home, TEMP: temp, TMP: temp, APPDATA: join(home, "AppData", "Roaming"), LOCALAPPDATA: join(home, "AppData", "Local") } : {}),
|
|
233
262
|
...extra,
|
|
234
263
|
};
|
|
235
264
|
}
|
|
@@ -249,7 +278,7 @@ const LIMITED = /usage limit|rate.?limit|quota|too many requests|429|out of cred
|
|
|
249
278
|
function runChild(bin, argv, { cwd, env, input, minutes, onLine }) {
|
|
250
279
|
return new Promise((done) => {
|
|
251
280
|
// A group leader, its group killed when it closes: nothing it started outlives the session.
|
|
252
|
-
const child = spawn(bin, argv, { cwd, env, stdio: ["pipe", "pipe", "pipe"],
|
|
281
|
+
const child = spawn(bin, argv, { cwd, env, stdio: ["pipe", "pipe", "pipe"], ...GROUP });
|
|
253
282
|
const reap = () => killGroup(child.pid);
|
|
254
283
|
process.once("exit", reap);
|
|
255
284
|
child.on("exit", reap);
|
|
@@ -437,7 +466,7 @@ async function execBackend({ cwd, text, transcript, kind, resume, chain, command
|
|
|
437
466
|
return { ...base, ok: false, exitCode: null, timedOut: false, durationMs: 0, subtype: "error_during_execution", finalText, stderr: "" };
|
|
438
467
|
}
|
|
439
468
|
const state = stateOf(transcript, resume);
|
|
440
|
-
const [bin, argv] = placement("
|
|
469
|
+
const [bin, argv] = placement(shellBin("sh"), ["-c", line], { cwd, state });
|
|
441
470
|
const passed = (process.env.INDEPENDENT_TESTS_EXEC_ENV ?? "").split(",").filter((key) => key && process.env[key] !== undefined);
|
|
442
471
|
const env = childEnv(state, Object.fromEntries(passed.map((key) => [key, process.env[key]])));
|
|
443
472
|
write({ type: "system", subtype: "init", session_id: sessionId, model: null, cwd, backend: "exec", command: line });
|
|
@@ -559,7 +588,7 @@ function fileTools(root, runBash) {
|
|
|
559
588
|
if (!inside(real(full), realRoot)) throw new Error(`${path} is outside the working directory ${root}`);
|
|
560
589
|
return full;
|
|
561
590
|
};
|
|
562
|
-
const shown = (full) => relative(root, full) || ".";
|
|
591
|
+
const shown = (full) => slash(relative(root, full)) || ".";
|
|
563
592
|
return {
|
|
564
593
|
Bash: (input) => runBash(String(input.command ?? ""), Math.min(Number(input.timeout ?? 120_000), 600_000)),
|
|
565
594
|
Read: (input) => {
|
|
@@ -592,7 +621,7 @@ function fileTools(root, runBash) {
|
|
|
592
621
|
const base = confined(input.path);
|
|
593
622
|
const matches = globRegExp(String(input.pattern));
|
|
594
623
|
const hits = listFiles(base).filter((path) => matches.test(path) || (!String(input.pattern).includes("/") && matches.test(basename(path))));
|
|
595
|
-
return hits.slice(0, 500).map((path) => join(shown(base) === "." ? "" : shown(base), path)).join("\n") || "no files";
|
|
624
|
+
return hits.slice(0, 500).map((path) => posix.join(shown(base) === "." ? "" : shown(base), path)).join("\n") || "no files";
|
|
596
625
|
},
|
|
597
626
|
Grep: (input) => {
|
|
598
627
|
const base = confined(input.path);
|
|
@@ -601,7 +630,7 @@ function fileTools(root, runBash) {
|
|
|
601
630
|
const out = [];
|
|
602
631
|
const files = statSync(base).isDirectory() ? listFiles(base).map((path) => join(base, path)) : [base];
|
|
603
632
|
for (const full of files) {
|
|
604
|
-
if (only !== null && !only.test(relative(base, full)) && !only.test(basename(full))) continue;
|
|
633
|
+
if (only !== null && !only.test(slash(relative(base, full))) && !only.test(basename(full))) continue;
|
|
605
634
|
if (statSync(full).size > 1_000_000) continue;
|
|
606
635
|
const lines = readFileSync(full, "utf8").split("\n");
|
|
607
636
|
lines.forEach((line, at) => {
|
|
@@ -629,10 +658,11 @@ async function anthropicApi({ cwd, text, transcript, kind, resume, model, maxTur
|
|
|
629
658
|
const started = Date.now();
|
|
630
659
|
const deadline = started + minutes * 60_000;
|
|
631
660
|
const runBash = (command, timeout) => {
|
|
632
|
-
const [bin, argv] = placement("
|
|
661
|
+
const [bin, argv] = placement(shellBin("bash"), ["-c", command], { cwd, state, network: false });
|
|
633
662
|
return new Promise((done) => {
|
|
634
663
|
let output = "";
|
|
635
|
-
const child = spawn(bin, argv, { cwd, env: childEnv(state), stdio: ["ignore", "pipe", "pipe"] });
|
|
664
|
+
const child = spawn(bin, argv, { cwd, env: childEnv(state), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
665
|
+
|
|
636
666
|
const take = (chunk) => (output = (output + chunk).slice(-30_000));
|
|
637
667
|
child.stdout.on("data", take);
|
|
638
668
|
child.stderr.on("data", take);
|
package/bin/generate.mjs
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// uses the same model as every other session.
|
|
30
30
|
|
|
31
31
|
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
32
|
-
import {
|
|
32
|
+
import { join, posix } from "node:path";
|
|
33
33
|
import {
|
|
34
34
|
RUNS,
|
|
35
35
|
args,
|
|
@@ -74,7 +74,7 @@ function suitesOf(chain) {
|
|
|
74
74
|
dir: "tests/public",
|
|
75
75
|
folder: publicFolder,
|
|
76
76
|
own: repo
|
|
77
|
-
? (path) => dirname(path) === publicFolder && chain.isPublicName(basename(path)) && test(path)
|
|
77
|
+
? (path) => posix.dirname(path) === publicFolder && chain.isPublicName(posix.basename(path)) && test(path)
|
|
78
78
|
: (path) => path.startsWith(`${publicFolder}/`) && test(path),
|
|
79
79
|
required: repo ? `${publicFolder}/${chain.publicName} (${chain.testPattern})` : `${publicFolder}/${chain.testPattern}`,
|
|
80
80
|
prompt: "public-tests",
|
|
@@ -255,7 +255,7 @@ async function generate(slug, force, only = null) {
|
|
|
255
255
|
if (record[suite.name].problem === null) {
|
|
256
256
|
rmSync(join(out, target), { recursive: true, force: true });
|
|
257
257
|
const files = Object.keys(manifestOf(dir, chain)).filter(inSuite);
|
|
258
|
-
for (const path of files) copy(join(dir, path), join(out, target, relative(suite.folder, path)));
|
|
258
|
+
for (const path of files) copy(join(dir, path), join(out, target, posix.relative(suite.folder, path)));
|
|
259
259
|
record[suite.name].files = files;
|
|
260
260
|
// A hidden suite written against an older public suite is stale.
|
|
261
261
|
if (suite.name === "public")
|
package/bin/handout.mjs
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { spawnSync } from "node:child_process";
|
|
27
27
|
import { commitment, recordCommitment } from "./commitment.mjs";
|
|
28
28
|
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
29
|
-
import { join, resolve } from "node:path";
|
|
29
|
+
import { join, posix, resolve, sep } from "node:path";
|
|
30
30
|
import {
|
|
31
31
|
AGENT,
|
|
32
32
|
PRIVATE,
|
|
@@ -58,7 +58,7 @@ if (options.repo !== undefined) {
|
|
|
58
58
|
process.exit(0);
|
|
59
59
|
}
|
|
60
60
|
const dir = resolve(target);
|
|
61
|
-
const beside = PRIVATE.find((folder) => dir === folder || dir.startsWith(`${folder}
|
|
61
|
+
const beside = PRIVATE.find((folder) => dir === folder || dir.startsWith(`${folder}${sep}`));
|
|
62
62
|
if (beside !== undefined) {
|
|
63
63
|
console.error(`${dir} is inside ${beside}, beside the hidden suites; pick a folder outside it.`);
|
|
64
64
|
process.exit(1);
|
|
@@ -112,7 +112,7 @@ function repoHandout(checkoutPath, remove) {
|
|
|
112
112
|
const files = [
|
|
113
113
|
...chain.seam.map((path) => ({ path, from: join(spec, "skeleton", path), seam: true })),
|
|
114
114
|
...listFiles(join(spec, "tests", "public")).map((name) => ({
|
|
115
|
-
path: join(chain.publicDir, name),
|
|
115
|
+
path: posix.join(chain.publicDir, name),
|
|
116
116
|
from: join(spec, "tests", "public", name),
|
|
117
117
|
seam: false,
|
|
118
118
|
})),
|