test(chaos): add R1, a code change in a fresh clone of a real repository (--workspace-repo)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 committed 2026-11-08 15:23:09 -06:00
1 parent 776f4cb602
commit cf38002439
10 files changed
+127 -8

No files matched your search

+16
View File
@@ -102,6 +102,22 @@ Neither G1 nor H1 is in the default workloads. The board's actions and what the
observer saw are in the timeline (`governance_*`) and in each task's grade
(`governance`).
## Code change in a real repository (R1, `--workspace-repo`)
```sh
node cli/node_modules/tsx/dist/cli.mjs tests/restart-chaos/main.ts --env local \
--adapters codex,claude --workloads R1,C1 --workspace-repo /path/to/paperclip \
--faults K1,K2,K4 --concurrency 4 --tasks 8 --seed 22
```
The harness clones the given git repository (shallow, from its committed HEAD)
as the project's workspace, in place of the chaoscli fixture. R1 asks the agent
to add a TypeScript module and a `node:test` file under
`packages/shared/src/chaos/`, named with the task's nonce so concurrent tasks
share the repository without colliding. The oracle runs the agent's test and a
hidden case against its module. B1 and B5 need the chaoscli fixture, so they do
not combine with `--workspace-repo`. Local only.
## Upgrade mid-run (K14, `--upgrade-from`)
```sh
+12 -2
View File
@@ -96,13 +96,14 @@ export function parseCommand(argv: readonly string[], now = Date.now()): ParsedC
"crash-point": { type: "string" },
"crash-point-hold-ms": { type: "string" },
"upgrade-from": { type: "string" },
"workspace-repo": { type: "string" },
},
});
const keep = values.keep === true;
const pollMs = integer(values["poll-ms"], "poll-ms", 3_000, 2_000, 5_000);
const budgetCents = integer(values["budget-cents"], "budget-cents", 3_000, 1, 100_000);
if (values.replay !== undefined) {
for (const key of ["env", "adapters", "workloads", "faults", "triggers", "concurrency", "tasks", "seed", "daytona-image", "daytona-cpu", "daytona-reuse-lease", "storm-minutes", "lifecycle", "k4-hold-max-seconds", "crash-point", "crash-point-hold-ms", "upgrade-from"] as const) {
for (const key of ["env", "adapters", "workloads", "faults", "triggers", "concurrency", "tasks", "seed", "daytona-image", "daytona-cpu", "daytona-reuse-lease", "storm-minutes", "lifecycle", "k4-hold-max-seconds", "crash-point", "crash-point-hold-ms", "upgrade-from", "workspace-repo"] as const) {
if (values[key] !== undefined) throw new Error(`--replay reproduces the saved plan; do not combine it with --${key}`);
}
return { mode: "replay", ref: parseReplayRef(values.replay), keep,
@@ -138,7 +139,7 @@ export function parseCommand(argv: readonly string[], now = Date.now()): ParsedC
env,
adapters: list<AdapterId>(values.adapters, ADAPTER_IDS, "adapters", ["codex", "claude"]),
// The governance scenarios (G1, H1) are opt-in: H1 needs a wave of its own.
workloads: list<WorkloadId>(values.workloads, workloadChoices, "workloads", workloadChoices.filter(id => id !== "G1" && id !== "H1")),
workloads: list<WorkloadId>(values.workloads, workloadChoices, "workloads", workloadChoices.filter(id => id !== "G1" && id !== "H1" && id !== "R1")),
// A crash-point wave schedules no fault; K1 is the restart its self-kill gets.
faults: list<FaultKind>(values.faults, FAULT_KINDS, "faults", stormMinutes ? STORM_DEFAULT_FAULTS : crashPoint ? ["K1"] : ["K1", "K2", "K4"]),
triggers: list<TriggerId>(values.triggers, TRIGGER_IDS, "triggers", ["time"]),
@@ -170,6 +171,15 @@ export function parseCommand(argv: readonly string[], now = Date.now()): ParsedC
if (crashPoint) throw new Error("--upgrade-from does not combine with --crash-point");
options.upgradeFrom = path.resolve(upgradeFrom);
}
const workspaceRepo = values["workspace-repo"];
if (options.workloads.includes("R1") !== (workspaceRepo !== undefined)) {
throw new Error("R1 (code change in a cloned repository) and --workspace-repo <git repository> go together: pass both or neither");
}
if (workspaceRepo !== undefined) {
if (env !== "local") throw new Error("--workspace-repo runs only with --env local");
if (options.workloads.some(id => id === "B1" || id === "B5")) throw new Error("--workspace-repo replaces the chaoscli fixture that B1 and B5 need");
options.workspaceRepo = path.resolve(workspaceRepo);
}
// H1 pauses the whole company when its budget hard stop is crossed: no other task can share its
// wave, its tasks run one at a time, and the hard stop must be on.
if (options.workloads.includes("H1")) {
+11
View File
@@ -229,6 +229,17 @@ describe("T-oracles", () => {
expect(workloadDefinitions.B1.grade("abcd1234", { ...base, commands: { ...base.commands, unittest: { exitCode: 1, stdout: "", stderr: "FAILED" } } }).verdict).toBe("fail");
});
it("grades R1 by running the agent's node test and a hidden case against its module", () => {
const base: OracleArtifacts = { issueStatus: "done", documents: {}, agentComments: [], workspacePath: "/tmp/ws",
files: { "packages/shared/src/chaos/slug-abcd1234.ts": "export function chaosSlug() {}",
"packages/shared/src/chaos/slug-abcd1234.test.ts": "import test from \"node:test\";" },
commands: { node_test: { exitCode: 0, stdout: "", stderr: "" }, slug: { exitCode: 0, stdout: "r1-paperclip-chaos-2026\n", stderr: "" } } };
expect(workloadDefinitions.R1.grade("abcd1234", base).verdict).toBe("pass");
expect(workloadDefinitions.R1.grade("abcd1234", { ...base, commands: { ...base.commands, slug: { exitCode: 0, stdout: "-r1-paperclip-chaos-2026-\n", stderr: "" } } }).verdict).toBe("fail");
const argv = workloadDefinitions.R1.collect.commands.find(command => command.name === "node_test")!.argv;
expect(typeof argv === "function" ? argv("abcd1234") : argv).toEqual(["node", "--test", "packages/shared/src/chaos/slug-abcd1234.test.ts"]);
});
it("grades B5 against the deterministic bench result", () => {
const base: OracleArtifacts = { issueStatus: "done", documents: {}, agentComments: [], workspacePath: "/tmp/ws", commands: {},
benchProgressObserved: 6,
+1 -1
View File
@@ -241,7 +241,7 @@ async function main(): Promise<number> {
adapters: [...new Set([...plan.tasks.map(task => task.adapter),
...(plan.tasks.some(task => task.workload === "M1") ? plan.options.adapters : [])])],
concurrency: plan.options.concurrency, budgetCents: plan.options.budgetCents, masks: plan.options.masks,
fixtureRoot: path.join(supervisor.paths.temporaryRoot, "fixtures"), waveId: plan.waveId, timeline,
fixtureRoot: path.join(supervisor.paths.temporaryRoot, "fixtures"), workspaceRepo: plan.options.workspaceRepo, waveId: plan.waveId, timeline,
adapterConfigOverrides: chaosLifecycleOverrides(plan.options.lifecycle),
onEnvironment: owned => {
sweeper = new DaytonaWaveSweeper({ apiKey: process.env.DAYTONA_API_KEY!, companyId: owned.companyId, environmentId: owned.environmentId, api });
+4 -1
View File
@@ -758,7 +758,10 @@ export class TaskObserver {
files[relative] = await readFile(path.join(workspacePath, relative), "utf8").catch(() => null);
}
// Agent-editable code: run it sandboxed, away from the operator's home directory.
for (const command of definition.collect.commands) commands[command.name] = await runOracleCommand(command.argv, workspacePath, { protectedRoots: this.protectedRoots });
for (const command of definition.collect.commands) {
const argv = typeof command.argv === "function" ? command.argv(this.plan.nonce) : command.argv;
commands[command.name] = await runOracleCommand(argv, workspacePath, { protectedRoots: this.protectedRoots });
}
}
return {
issueStatus: issue?.status ?? this.record.finalStatus,
+12
View File
@@ -46,6 +46,18 @@ describe("scheduler determinism", () => {
expect(() => parseCommand(["--replay", "w-20261106T101112-s42/t03", "--k4-hold-max-seconds", "400"])).toThrow(/do not combine/);
});
it("pairs R1 with --workspace-repo, locally, without the chaoscli fixture workloads", () => {
const command = parseCommand(["--workloads", "R1,C1", "--workspace-repo", "../paperclip-clone"]);
if (command.mode !== "wave") throw new Error("expected a wave");
expect(command.options.workspaceRepo!.endsWith("paperclip-clone")).toBe(true);
expect(() => parseCommand(["--workloads", "R1"])).toThrow(/go together/);
expect(() => parseCommand(["--workloads", "A1", "--workspace-repo", "x"])).toThrow(/go together/);
expect(() => parseCommand(["--workloads", "R1,B1", "--workspace-repo", "x"])).toThrow(/chaoscli fixture/);
const defaults = parseCommand([]);
if (defaults.mode !== "wave") throw new Error("expected a wave");
expect(defaults.options.workloads).not.toContain("R1");
});
it("pairs K14 (upgrade mid-run) with --upgrade-from, locally, with no hold and outside storm defaults", () => {
const command = parseCommand(["--faults", "K14", "--upgrade-from", "../older-checkout", "--seed", "5"]);
if (command.mode !== "wave") throw new Error("expected a wave");
+19 -1
View File
@@ -1,9 +1,11 @@
import { execFileSync } from "node:child_process";
import { existsSync } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ChaosApi } from "./api.js";
import { seedChaosLab } from "./seed.js";
import { cloneWorkspaceRepository, createFixtureRepository, seedChaosLab } from "./seed.js";
import { Timeline } from "./timeline.js";
type Call = { method: string; route: string; body?: unknown };
@@ -104,3 +106,19 @@ describe("seedChaosLab RC-06 mask", () => {
expect(timeline.entries.some(entry => entry.kind === "seed_mask_rc06")).toBe(false);
});
});
describe("cloneWorkspaceRepository", () => {
it("clones a fresh, shallow working copy of the source repository", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "restart-chaos-clone-"));
try {
const source = await createFixtureRepository(path.join(root, "source"));
const clone = cloneWorkspaceRepository(source, path.join(root, "clone"));
expect(existsSync(path.join(clone, "chaoscli/cli.py"))).toBe(true);
expect(execFileSync("git", ["rev-parse", "--is-shallow-repository"], { cwd: clone, encoding: "utf8" }).trim()).toBe("true");
expect(execFileSync("git", ["rev-parse", "HEAD"], { cwd: clone, encoding: "utf8" }))
.toBe(execFileSync("git", ["rev-parse", "HEAD"], { cwd: source, encoding: "utf8" }));
} finally {
await rm(root, { recursive: true, force: true });
}
});
});
+9 -1
View File
@@ -22,6 +22,11 @@ export interface ChaosLab {
}
/** Create the `chaos-sandbox` git repository: python CLI, one failing test, bench.sh. */
export function cloneWorkspaceRepository(source: string, directory: string): string {
execFileSync("git", ["clone", "--quiet", "--depth", "1", "--no-hardlinks", `file://${source}`, directory], { stdio: "pipe" });
return directory;
}
export async function createFixtureRepository(directory: string, benchSeconds?: number): Promise<string> {
await mkdir(directory, { recursive: true });
for (const [relative, file] of Object.entries(fixtureRepositoryFiles(benchSeconds))) {
@@ -116,6 +121,7 @@ export async function seedChaosLab(input: {
budgetCents: number;
masks?: readonly string[];
fixtureRoot: string;
workspaceRepo?: string;
waveId: string;
timeline: Timeline;
/** Merged into each agent's adapterConfig (for example a runner lifecycle mode). */
@@ -130,7 +136,9 @@ export async function seedChaosLab(input: {
await installFixtureDaytonaProvider(runnerApiView(input.api), { environment: { id: "daytona" } } as MatrixExecution);
timeline.add("seed_daytona_plugin", {});
}
const fixtureRepositoryPath = await createFixtureRepository(path.join(input.fixtureRoot, "chaos-sandbox"));
const fixtureRepositoryPath = input.workspaceRepo
? cloneWorkspaceRepository(input.workspaceRepo, path.join(input.fixtureRoot, "workspace-repo"))
: await createFixtureRepository(path.join(input.fixtureRoot, "chaos-sandbox"));
const company = await api.post<{ id: string }>("/api/companies", {
name: "Chaos Lab",
description: `Restart chaos harness wave ${input.waveId}`,
+2 -1
View File
@@ -7,7 +7,7 @@ export type FaultKind = (typeof FAULT_KINDS)[number];
export type TaskFaultKind = FaultKind | "K13";
/** G1: an approval-gated action across a restart. H1: a budget hard stop crossed mid-run with a restart (its own wave). */
export const WORKLOAD_IDS = ["A1", "B1", "B5", "C1", "M1", "G1", "H1"] as const;
export const WORKLOAD_IDS = ["A1", "B1", "B5", "C1", "M1", "G1", "H1", "R1"] as const;
export type WorkloadId = (typeof WORKLOAD_IDS)[number];
export const ADAPTER_IDS = ["codex", "claude", "opencode"] as const;
@@ -72,6 +72,7 @@ export interface WaveOptions {
* and instance directory. Later K14 faults restart the current build, like K2.
*/
upgradeFrom?: string;
workspaceRepo?: string;
}
export interface StormFault {
+41 -1
View File
@@ -17,7 +17,7 @@ export interface WorkloadDefinition {
description(nonce: string, agents?: WorkloadAgents): string;
followUp?(nonce: string): string;
/** Files and commands the observer must collect for the T-oracle. */
collect: { documents: string[]; files(nonce: string): string[]; commands: Array<{ name: string; argv: string[] }>;
collect: { documents: string[]; files(nonce: string): string[]; commands: Array<{ name: string; argv: string[] | ((nonce: string) => string[]) }>;
childDocuments?: string[] };
grade(nonce: string, artifacts: OracleArtifacts, agents?: WorkloadAgents): OracleResult;
}
@@ -38,6 +38,8 @@ const followUpMarker = (nonce: string) => `FOLLOWUP-${nonce}`;
const notesFile = (nonce: string) => `NOTES-${nonce}.md`;
const benchFile = (nonce: string) => `bench-result-${nonce}.txt`;
const benchRecord = (nonce: string) => `.bench/run-${nonce}.txt`;
const r1Module = (nonce: string) => `packages/shared/src/chaos/slug-${nonce}.ts`;
const r1Test = (nonce: string) => `packages/shared/src/chaos/slug-${nonce}.test.ts`;
/** bench.sh must have actually run: at least this long between start and finish. */
export const BENCH_MIN_SECONDS = 200;
/** B5: distinct progress values the harness must have observed itself while polling. */
@@ -264,6 +266,44 @@ export const workloadDefinitions: Record<WorkloadId, WorkloadDefinition> = {
]);
},
},
R1: {
id: "R1",
label: "Code change in a Paperclip clone",
usesRepository: true,
fireWindowMs: [20_000, 90_000],
budgetMs: 15 * 60_000,
title: nonce => `Chaos R1 slug module ${nonce}`,
description: nonce => [
"Work in this task's repository workspace, a clone of the Paperclip repository.",
`Add \`${r1Module(nonce)}\` exporting \`chaosSlug(input: string): string\`: lowercase, every run of characters`,
"other than a-z and 0-9 becomes a single `-`, and no leading or trailing `-`.",
`Add \`${r1Test(nonce)}\` that uses \`node:test\` and \`node:assert/strict\`, imports \`./slug-${nonce}.ts\`,`,
"and checks at least three cases, including `chaosSlug(\"Hello Chaos World\") === \"hello-chaos-world\"`.",
`Run \`node --test ${r1Test(nonce)}\` from the repository root until it passes.`,
"Do not install dependencies and do not change any other file. Then mark this task done.",
].join("\n"),
collect: {
documents: [],
files: nonce => [r1Module(nonce), r1Test(nonce)],
commands: [
{ name: "node_test", argv: nonce => ["node", "--test", r1Test(nonce)] },
{ name: "slug", argv: nonce => ["node", "--input-type=module", "-e",
`import { chaosSlug } from "./${r1Module(nonce)}"; console.log(chaosSlug(" R1 -- Paperclip__Chaos 2026! "));`] },
],
},
grade(nonce, artifacts) {
const tests = artifacts.commands.node_test;
const slug = artifacts.commands.slug;
return result([
statusCheck(artifacts),
{ name: "workspace_located", ok: artifacts.workspacePath !== null },
{ name: "module_present", ok: typeof artifacts.files[r1Module(nonce)] === "string" },
{ name: "test_present", ok: (artifacts.files[r1Test(nonce)] ?? "").includes("node:test") },
{ name: "node_test_passes", ok: tests?.exitCode === 0, detail: `exit=${tests?.exitCode ?? "not run"}` },
{ name: "hidden_case", ok: slug?.exitCode === 0 && slug.stdout.trim() === "r1-paperclip-chaos-2026", detail: JSON.stringify(slug?.stdout.trim() ?? null) },
]);
},
},
B5: {
id: "B5",
label: "Long bench.sh run",