@agentsfleet/orly 0.12.0 → 0.13.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.
@@ -0,0 +1,166 @@
1
+ import { expect, test } from "bun:test";
2
+
3
+ import { adoptionDecision, missingObservations, scoreQuestion, validateObservations } from "../evals/judgments/comparison/score";
4
+ import { ADJUDICATION, CANDIDATE, CASE_CLASS, CHECK_ERROR, CLASSIFICATION, LABEL_AUTHORITY, MEASUREMENT, NATIVE, OBSERVATION, SPLIT,
5
+ type ComparisonCase, type IndependentLabel, type Observation } from "../evals/judgments/comparison/types";
6
+ import { CHOICE, MODEL, QUESTIONS } from "./judgments/constants";
7
+ import { OrlyError } from "./model";
8
+
9
+ const CERTAIN = 1;
10
+ const UNCERTAIN = 0.5;
11
+ const TEST_AUTHOR = "score-fixture-author";
12
+ const TEST_ORACLE = "unit-boundary-oracle";
13
+ const DIGEST_LENGTH = 64;
14
+ const REVISION_LENGTH = 40;
15
+ const REFERENCE_SOURCE = "reference.md";
16
+
17
+ function fixture(question: ComparisonCase["question"], classifications: ComparisonCase["classification"][], expected: IndependentLabel["expected_native"][]) {
18
+ const cases: ComparisonCase[] = classifications.map((classification, index) => ({ id: `case-${index}`, question, classification,
19
+ origin: `family-${index}`, split: SPLIT.heldout, author: TEST_AUTHOR, requirement: "Assert the sourced result.", evidence: [] }));
20
+ const labels: IndependentLabel[] = cases.map((entry, index) => {
21
+ const answer = expected[index];
22
+ if (!answer) throw new Error("Missing unit expectation.");
23
+ return { case_id: entry.id, classification: entry.classification, expected_native: answer, rationale: "Exact unit boundary expectation.",
24
+ author: TEST_AUTHOR, adjudication: ADJUDICATION.resolved, authority: { kind: LABEL_AUTHORITY.oracle, name: TEST_ORACLE },
25
+ source: { path: REFERENCE_SOURCE, digest: "0".repeat(DIGEST_LENGTH), revision: "0".repeat(REVISION_LENGTH), selected: "Expected result." } };
26
+ });
27
+ const observations: Observation[] = missingObservations(cases).map((entry, index) => {
28
+ const decision = expected[index];
29
+ if (!decision) throw new Error("Missing unit answer.");
30
+ return { ...entry, status: OBSERVATION.valid, answer: { decision, strength: CERTAIN } };
31
+ });
32
+ return { cases, labels, observations };
33
+ }
34
+
35
+ test("metrics_preserve_missing_outcomes", () => {
36
+ const { cases, labels, observations } = fixture(QUESTIONS[0], [CASE_CLASS.healthy, CASE_CLASS.defective, CASE_CLASS.ambiguous, CASE_CLASS.insufficient],
37
+ [NATIVE.yes, NATIVE.no, NATIVE.abstain, NATIVE.abstain]);
38
+ const mixed = observations.map((item, index) => index === 0 ? { ...item, status: OBSERVATION.unavailable, answer: null } : item);
39
+ const result = scoreQuestion(QUESTIONS[0], cases, labels, validateObservations(cases, mixed));
40
+ expect(result.native_correctness).toEqual({ numerator: 3, denominator: 4, value: 0.75 });
41
+ expect(result.defect_recall).toEqual({ numerator: 1, denominator: 1, value: 1 });
42
+ expect(result.false_concerns_bound).toEqual({ numerator: 1, denominator: 1, value: 1 });
43
+ expect(result.insufficient_detection).toBeNull();
44
+ expect(result.shared_abstention).toEqual({ numerator: 2, denominator: 2, value: 1 });
45
+ expect(() => validateObservations(cases, mixed.slice(1))).toThrow(new OrlyError(CHECK_ERROR.observations));
46
+ });
47
+
48
+ test("healthy_applicable_yes_is_native_correctness_without_defect_metrics", () => {
49
+ const { cases, labels, observations } = fixture(QUESTIONS[4], [CASE_CLASS.healthy], [NATIVE.yes]);
50
+ const result = scoreQuestion(QUESTIONS[4], cases, labels, observations);
51
+ expect(result.native_correctness?.value).toBe(1);
52
+ expect(result.false_concerns).toBeNull();
53
+ expect(result.defect_recall).toBeNull();
54
+ });
55
+
56
+ test("noul_half_probability_abstains_without_inventing_insufficiency", () => {
57
+ const { cases, labels, observations } = fixture(QUESTIONS[0], [CASE_CLASS.insufficient], [NATIVE.abstain]);
58
+ const answer = observations.map((item) => ({ ...item, answer: { decision: NATIVE.yes, strength: UNCERTAIN } }));
59
+ const result = scoreQuestion(QUESTIONS[0], cases, labels, answer);
60
+ expect(result.shared_abstention?.value).toBe(1);
61
+ expect(result.insufficient_detection).toBeNull();
62
+ });
63
+
64
+ test("zero_responses_and_empty_denominators_are_unavailable", () => {
65
+ const { cases, labels } = fixture(CANDIDATE.obligation, [CASE_CLASS.healthy], [CLASSIFICATION.supported]);
66
+ const result = scoreQuestion(CANDIDATE.obligation, cases, labels, missingObservations(cases));
67
+ expect(result.attempted).toBe(1);
68
+ expect(result.unavailable).toBe(1);
69
+ expect(result.native_correctness).toBeNull();
70
+ expect(result.defect_recall).toBeNull();
71
+ expect(result.false_concerns_bound).toBeNull();
72
+ expect(scoreQuestion(CANDIDATE.wiring, [], [], []).native_correctness).toBeNull();
73
+ });
74
+
75
+ test("invalid_defective_answers_are_misses_and_invalid_healthy_answers_raise_the_bound", () => {
76
+ const { cases, labels, observations } = fixture(CANDIDATE.failure, [CASE_CLASS.healthy, CASE_CLASS.defective], [CLASSIFICATION.supported, CLASSIFICATION.defective]);
77
+ const answer = observations.map((item, index) => index === 0 ? item : { ...item, status: OBSERVATION.invalid, answer: null });
78
+ const result = scoreQuestion(CANDIDATE.failure, cases, labels, answer);
79
+ expect(result.native_correctness?.value).toBe(0.5);
80
+ expect(result.defect_recall?.value).toBe(0);
81
+ expect(result.invalid).toBe(1);
82
+ });
83
+
84
+ test("weak_low_confidence_assertion_is_an_abstention", () => {
85
+ const { cases, labels, observations } = fixture(QUESTIONS[2], [CASE_CLASS.defective], [CHOICE.weak]);
86
+ const answer = observations.map((item) => ({ ...item, answer: { decision: CHOICE.weak, strength: UNCERTAIN } }));
87
+ const result = scoreQuestion(QUESTIONS[2], cases, labels, answer);
88
+ expect(result.native_correctness?.value).toBe(0);
89
+ expect(result.defect_recall?.value).toBe(0);
90
+ });
91
+
92
+ test("explicit_choice_insufficiency_is_scored_separately", () => {
93
+ const { cases, labels, observations } = fixture(QUESTIONS[2], [CASE_CLASS.insufficient], [CHOICE.insufficient]);
94
+ expect(scoreQuestion(QUESTIONS[2], cases, labels, observations).insufficient_detection?.value).toBe(1);
95
+ });
96
+
97
+ test("observations_reject_stale_duplicate_unknown_and_executable_answers", () => {
98
+ const { cases, observations } = fixture(CANDIDATE.wiring, [CASE_CLASS.healthy, CASE_CLASS.defective], [CLASSIFICATION.supported, CLASSIFICATION.defective]);
99
+ expect(() => validateObservations(cases, observations.map((item) => ({ ...item, identity: "0".repeat(DIGEST_LENGTH) })))).toThrow(CHECK_ERROR.observations);
100
+ expect(() => validateObservations(cases, [observations[0], observations[0]])).toThrow(CHECK_ERROR.observations);
101
+ expect(() => validateObservations(cases, observations.map((item) => ({ ...item, execute: "true" })))).toThrow(CHECK_ERROR.observations);
102
+ expect(() => validateObservations(cases.map((item) => ({ ...item, requirement: "Edited required behavior." })), observations)).toThrow(CHECK_ERROR.observations);
103
+ });
104
+
105
+ test("adoption_requires_independent_measured_benefit", () => {
106
+ const { cases, labels, observations } = fixture(CANDIDATE.resolution, Object.values(CASE_CLASS), Object.values(CLASSIFICATION));
107
+ const decision = adoptionDecision(CANDIDATE.resolution, cases, labels, missingObservations(cases), null);
108
+ expect(decision.eligible).toBe(false);
109
+ expect(decision.reasons.length).toBe(4);
110
+ expect(decision.decision).toBe("retain");
111
+ expect(adoptionDecision(CANDIDATE.resolution, cases, labels, observations, null).eligible).toBe(false);
112
+ });
113
+
114
+ test("unmeasured_candidates_remain_evaluation_only", () => {
115
+ for (const question of Object.values(CANDIDATE)) {
116
+ const { cases, labels } = fixture(question, [CASE_CLASS.healthy], [CLASSIFICATION.supported]);
117
+ const result = adoptionDecision(question, cases, labels, missingObservations(cases), null);
118
+ expect(result.eligible).toBe(false);
119
+ expect(result.metrics.native_correctness).toBeNull();
120
+ }
121
+ expect(QUESTIONS.length).toBe(6);
122
+ });
123
+
124
+ test("development_cases_cannot_fill_the_heldout_adoption_minimum", () => {
125
+ const { cases, labels, observations } = fixture(CANDIDATE.wiring, [CASE_CLASS.healthy], [CLASSIFICATION.supported]);
126
+ const result = adoptionDecision(CANDIDATE.wiring, cases.map((entry) => ({ ...entry, split: SPLIT.development })), labels,
127
+ observations.map((entry) => ({ ...entry, measurement: MEASUREMENT.independent })), { baseline_resolved: 0, candidate_resolved: 1, unsupported_completions: 0, authority_violations: 0 });
128
+ expect(result.eligible).toBe(false);
129
+ expect(result.metrics.attempted).toBe(0);
130
+ });
131
+
132
+ test("answers_from_another_question_format_count_as_invalid_attempts", () => {
133
+ const { cases, labels, observations } = fixture(CANDIDATE.wiring, [CASE_CLASS.healthy], [CLASSIFICATION.supported]);
134
+ const malformed = observations.map((entry) => ({ ...entry, answer: { decision: NATIVE.yes, strength: CERTAIN } }));
135
+ const validated = validateObservations(cases, malformed);
136
+ expect(validated.map((entry) => entry.status)).toEqual([OBSERVATION.invalid]);
137
+ const result = scoreQuestion(CANDIDATE.wiring, cases, labels, malformed);
138
+ expect(result.invalid).toBe(1);
139
+ expect(result.valid).toBe(0);
140
+ expect(result.false_concerns_bound?.value).toBe(1);
141
+ });
142
+
143
+ test("pending_extra_cases_are_unscored_and_do_not_change_denominators", () => {
144
+ const { cases, labels, observations } = fixture(CANDIDATE.evidence, [CASE_CLASS.healthy, CASE_CLASS.healthy], [CLASSIFICATION.supported, CLASSIFICATION.supported]);
145
+ const pending = labels.map((entry, index) => index === 0 ? entry : { ...entry, adjudication: ADJUDICATION.pending });
146
+ const result = scoreQuestion(CANDIDATE.evidence, cases, pending, observations);
147
+ expect(result.attempted).toBe(1);
148
+ expect(result.unscored).toBe(1);
149
+ expect(result.native_correctness).toEqual({ numerator: 1, denominator: 1, value: 1 });
150
+ });
151
+
152
+ test("fabricated_measurement_enums_and_paired_counts_never_qualify_for_adoption", () => {
153
+ const minimum = 20;
154
+ const classes = Object.values(CASE_CLASS).flatMap((classification) => Array.from({ length: minimum }, () => classification));
155
+ const answers = Object.values(CLASSIFICATION).flatMap((answer) => Array.from({ length: minimum }, () => answer));
156
+ const { cases, labels, observations } = fixture(CANDIDATE.resolution, classes, answers);
157
+ const fabricated = observations.map((entry) => ({ ...entry, measurement: MEASUREMENT.independent }));
158
+ const invalidPair = { baseline_resolved: -1, candidate_resolved: 0, unsupported_completions: 0, authority_violations: 0 };
159
+ const ordinaryPair = { ...invalidPair, baseline_resolved: 1, candidate_resolved: 2 };
160
+ for (const pair of [invalidPair, ordinaryPair]) {
161
+ const result = adoptionDecision(CANDIDATE.resolution, cases, labels, fabricated, pair);
162
+ expect(result.eligible).toBe(false);
163
+ expect(result.decision).toBe("retain");
164
+ expect(result.reasons.some((reason) => reason.includes("provenance"))).toBe(true);
165
+ }
166
+ });
@@ -0,0 +1,164 @@
1
+ import { afterEach, expect, mock, spyOn, test } from "bun:test";
2
+ import * as fileSystem from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import { EvaluationRun, EXECUTION_ERROR, sourceIdentity } from "../evals/judgments/comparison/execution";
6
+ import { cleanupTemporaryDirectories, temporaryDirectory } from "./gates_test_support";
7
+ import { createOwnership, OWNER_ERROR, recoverStaleOwnership } from "../evals/judgments/comparison/recovery";
8
+
9
+ const INPUT_FILE = "input.txt";
10
+ const INITIAL_CONTENT = "2\n3\n";
11
+ const BUN_EVAL = "-e";
12
+ const COMMAND_SUCCESS = 'process.stdout.write("measured\\n")';
13
+ const COMMAND_FAILURE = 'process.stderr.write("required refusal\\n");process.exit(2)';
14
+ const COMMAND_WAIT = 'process.stdout.write("ready\\n");setInterval(()=>{},1000)';
15
+ const COMMAND_OUTPUT = 'process.stdout.write("x".repeat(1000000));setInterval(()=>{},1000)';
16
+ const SUCCESS_LIMITS = { timeout_ms: 2_000, output_bytes: 4_096 };
17
+ const DEADLINE_LIMITS = { timeout_ms: 150, output_bytes: SUCCESS_LIMITS.output_bytes };
18
+ const OUTPUT_LIMITS = { timeout_ms: SUCCESS_LIMITS.timeout_ms, output_bytes: 128 };
19
+ const TEST_TIMEOUT_MS = 10_000;
20
+ const READY_POLL_MS = 10;
21
+ const { existsSync } = fileSystem;
22
+ const START_SENTINEL = "child-started.txt";
23
+ const SENTINEL_COMMAND = `await Bun.write('${START_SENTINEL}','started')`;
24
+ const ALLOCATION_FAILURE = "second temporary-directory allocation failed";
25
+ type SyncOptions = NonNullable<Parameters<typeof Bun.spawnSync>[1]>;
26
+
27
+ afterEach(() => { mock.restore(); cleanupTemporaryDirectories(); });
28
+
29
+ test("task_receipts_bind_actual_execution", async () => {
30
+ const run = await EvaluationRun.create();
31
+ try {
32
+ await Bun.write(join(run.workspace, INPUT_FILE), INITIAL_CONTENT);
33
+ const actual = await run.execute([process.execPath, BUN_EVAL, COMMAND_SUCCESS], [INPUT_FILE], SUCCESS_LIMITS);
34
+ expect(actual.result.exit_code).toBe(0);
35
+ expect(actual.stdout).toBe("measured\n");
36
+ expect(actual.result.output_bytes).toBe(new TextEncoder().encode(actual.stdout).byteLength);
37
+ expect(actual.source_before).toBe(await sourceIdentity(run.workspace, [INPUT_FILE]));
38
+ expect(() => run.verify(actual)).not.toThrow();
39
+ expect(() => run.verify({ ...actual, result: { ...actual.result, exit_code: 2 } })).toThrow(EXECUTION_ERROR);
40
+ expect(() => run.verify({ ...actual, command: ["true"] })).toThrow(EXECUTION_ERROR);
41
+ await Bun.write(join(run.workspace, INPUT_FILE), "9\n");
42
+ expect(await sourceIdentity(run.workspace, [INPUT_FILE])).not.toBe(actual.source_after);
43
+ } finally { await run.close(); }
44
+ });
45
+
46
+ test("offline_tasks_bound_and_clean_owned_work", async () => {
47
+ const sentinel = join(temporaryDirectory(), "unrelated.txt");
48
+ await Bun.write(sentinel, INITIAL_CONTENT);
49
+ const controls = [
50
+ { command: COMMAND_SUCCESS, limits: SUCCESS_LIMITS, exit: 0, failure: undefined },
51
+ { command: COMMAND_FAILURE, limits: SUCCESS_LIMITS, exit: 2, failure: undefined },
52
+ { command: COMMAND_WAIT, limits: DEADLINE_LIMITS, exit: null, failure: "deadline exceeded" },
53
+ { command: COMMAND_OUTPUT, limits: OUTPUT_LIMITS, exit: null, failure: "output limit exceeded" },
54
+ ];
55
+ for (const control of controls) {
56
+ const run = await EvaluationRun.create();
57
+ const owned = run.ownership;
58
+ try {
59
+ const receipt = await run.execute([process.execPath, BUN_EVAL, control.command], [], control.limits);
60
+ run.verify(receipt);
61
+ if (control.failure === undefined) expect(receipt.result.exit_code).toBe(control.exit);
62
+ else expect(receipt.result.failure).toBe(control.failure);
63
+ expect(existsSync(receipt.receipt_directory)).toBe(false);
64
+ if (receipt.child_pid !== null) expect(() => process.kill(receipt.child_pid ?? 0, 0)).toThrow();
65
+ } finally { await run.close(); }
66
+ expect(existsSync(owned.workspace)).toBe(false);
67
+ expect(existsSync(owned.receipts)).toBe(false);
68
+ expect(await Bun.file(sentinel).text()).toBe(INITIAL_CONTENT);
69
+ }
70
+ }, TEST_TIMEOUT_MS);
71
+
72
+ test("handled_interruption_captures_failure_and_removes_owned_directories", async () => {
73
+ const run = await EvaluationRun.create();
74
+ const owned = run.ownership;
75
+ const controller = new AbortController();
76
+ let timer: ReturnType<typeof setTimeout> | undefined;
77
+ try {
78
+ timer = setTimeout(() => controller.abort(), DEADLINE_LIMITS.timeout_ms);
79
+ const receipt = await run.execute([process.execPath, BUN_EVAL, COMMAND_WAIT], [], SUCCESS_LIMITS, controller.signal);
80
+ expect(receipt.result.failure).toBe("supervisor received SIGTERM");
81
+ expect(receipt.stdout).toBe("ready\n");
82
+ if (receipt.child_pid !== null) expect(() => process.kill(receipt.child_pid ?? 0, 0)).toThrow();
83
+ } finally {
84
+ if (timer !== undefined) clearTimeout(timer);
85
+ await run.close();
86
+ }
87
+ expect(existsSync(owned.workspace)).toBe(false);
88
+ expect(existsSync(owned.receipts)).toBe(false);
89
+ }, TEST_TIMEOUT_MS);
90
+
91
+ test("recovery_refuses_live_owner_or_altered_ownership_without_deleting_resources", async () => {
92
+ const run = await EvaluationRun.create();
93
+ const owned = run.ownership;
94
+ try {
95
+ await expect(recoverStaleOwnership(owned)).rejects.toThrow(OWNER_ERROR);
96
+ await expect(recoverStaleOwnership({ ...owned, token: crypto.randomUUID() })).rejects.toThrow(OWNER_ERROR);
97
+ expect(existsSync(owned.workspace)).toBe(true);
98
+ expect(existsSync(owned.receipts)).toBe(true);
99
+ } finally { await run.close(); }
100
+ });
101
+
102
+ test("closed_evaluation_refuses_new_commands_and_double_close_is_safe", async () => {
103
+ const run = await EvaluationRun.create();
104
+ await run.close();
105
+ await run.close();
106
+ await expect(run.execute([process.execPath, BUN_EVAL, COMMAND_SUCCESS], [], SUCCESS_LIMITS)).rejects.toThrow(EXECUTION_ERROR);
107
+ });
108
+
109
+ test("cancellation_during_request_preparation_prevents_any_child_start", async () => {
110
+ const run = await EvaluationRun.create();
111
+ const controller = new AbortController();
112
+ const write = Bun.write.bind(Bun);
113
+ let injections = 0;
114
+ try {
115
+ spyOn(Bun, "write").mockImplementation(async (destination, input, settings) => {
116
+ if (String(destination).endsWith("/request.json")) { injections++; controller.abort(); }
117
+ return Reflect.apply(write, Bun, [destination, input, settings]);
118
+ });
119
+ await expect(run.execute([process.execPath, BUN_EVAL, SENTINEL_COMMAND], [], SUCCESS_LIMITS, controller.signal)).rejects.toThrow(EXECUTION_ERROR);
120
+ expect(injections).toBe(1);
121
+ expect(existsSync(join(run.workspace, START_SENTINEL))).toBe(false);
122
+ } finally { mock.restore(); await run.close(); }
123
+ });
124
+
125
+ test("failed_process_inspection_never_authorizes_live_owner_cleanup", async () => {
126
+ const run = await EvaluationRun.create();
127
+ const owned = run.ownership;
128
+ const spawn = Bun.spawnSync.bind(Bun);
129
+ let inspections = 0;
130
+ try {
131
+ spyOn(Bun, "spawnSync").mockImplementation((argument: string[] | (SyncOptions & { cmd: string[] }), options?: SyncOptions) => {
132
+ const actual = Reflect.apply(spawn, Bun, Array.isArray(argument) ? [argument, options] : [argument]);
133
+ const argv = Array.isArray(argument) ? argument : argument.cmd;
134
+ if (Array.isArray(argv) && argv[0] === "ps") { inspections++; return { ...actual, exitCode: 1, stdout: Buffer.alloc(0) }; }
135
+ return actual;
136
+ });
137
+ await expect(recoverStaleOwnership(owned)).rejects.toThrow(OWNER_ERROR);
138
+ expect(inspections).toBe(1);
139
+ expect(existsSync(owned.workspace)).toBe(true);
140
+ expect(existsSync(owned.receipts)).toBe(true);
141
+ } finally { mock.restore(); await run.close(); }
142
+ });
143
+
144
+ test("failed_second_directory_allocation_cleans_the_real_first_directory", async () => {
145
+ const allocate = fileSystem.mkdtempSync;
146
+ const allocated: string[] = [];
147
+ let attempts = 0;
148
+ try {
149
+ spyOn(fileSystem, "mkdtempSync").mockImplementation((prefix, options) => {
150
+ attempts++;
151
+ if (attempts === 2) throw new Error(ALLOCATION_FAILURE);
152
+ const path = Reflect.apply(allocate, fileSystem, [prefix, options]);
153
+ if (typeof path === "string") allocated.push(path);
154
+ return path;
155
+ });
156
+ await expect(createOwnership()).rejects.toThrow(ALLOCATION_FAILURE);
157
+ expect(attempts).toBe(2);
158
+ expect(allocated.length).toBe(1);
159
+ for (const path of allocated) expect(existsSync(path)).toBe(false);
160
+ } finally {
161
+ mock.restore();
162
+ for (const path of allocated) if (existsSync(path)) fileSystem.rmSync(path, { recursive: true, force: true });
163
+ }
164
+ });
@@ -0,0 +1,76 @@
1
+ import { expect, test } from "bun:test";
2
+ import { join, resolve } from "node:path";
3
+
4
+ import { EvaluationRun } from "../evals/judgments/comparison/execution";
5
+ import { checkWholeTask } from "../evals/judgments/comparison/hidden/assertions";
6
+ import { CORRECT_COMMAND, CORRECT_HELPER, EXACT_TEST } from "../evals/judgments/comparison/task-fixtures";
7
+ import { loadTasks, OWNER_CONTENT } from "../evals/judgments/comparison/tasks";
8
+ import { TASK_FAMILY, TASK_FILES } from "../evals/judgments/comparison/types";
9
+
10
+ const ROOT = resolve(import.meta.dir, "..");
11
+ const TEST_TIMEOUT = 30_000;
12
+
13
+ for (const family of [TASK_FAMILY.wiring, TASK_FAMILY.failure]) {
14
+ test(`hidden_checks_reject_inline_completion_without_required_${family}`, async () => {
15
+ const task = (await loadTasks(ROOT)).find((entry) => entry.id === family);
16
+ if (!task) throw new Error("Missing task family.");
17
+ const run = await EvaluationRun.create();
18
+ try {
19
+ await Bun.write(join(run.workspace, TASK_FILES.entry), CORRECT_COMMAND);
20
+ await Bun.write(join(run.workspace, TASK_FILES.helper), CORRECT_HELPER);
21
+ await Bun.write(join(run.workspace, TASK_FILES.linked), EXACT_TEST);
22
+ await Bun.write(join(run.workspace, TASK_FILES.owner), OWNER_CONTENT);
23
+ const result = await checkWholeTask(run, task, OWNER_CONTENT);
24
+ expect(result.passed).toBe(false);
25
+ expect(result.checks.some((check) => !check.passed)).toBe(true);
26
+ } finally { await run.close(); }
27
+ }, TEST_TIMEOUT);
28
+ }
29
+
30
+ test("consumer_hidden_evidence_refuses_missing_malformed_and_unrelated_checks", async () => {
31
+ const { validConsumerChecks } = await import("../evals/judgments/comparison/consumer-evidence");
32
+ for (const stdout of ["", "SyntaxError: unexpected token", "[]", JSON.stringify([{ name: "unrelated", passed: false }])]) {
33
+ expect(validConsumerChecks(stdout, true)).toBe(false);
34
+ }
35
+ expect(validConsumerChecks(JSON.stringify([{ name: "output-exists", passed: true }]), false)).toBe(true);
36
+ expect(validConsumerChecks(JSON.stringify([{ name: "output-exists", passed: false }]), false)).toBe(false);
37
+ });
38
+
39
+ test("consumer_package_identity_changes_when_same_version_source_changes", async () => {
40
+ const { packageIdentity } = await import("../evals/judgments/comparison/consumer-evidence");
41
+ const run = await EvaluationRun.create();
42
+ try {
43
+ const source = join(run.workspace, "entry.ts");
44
+ await Bun.write(source, "export const value = true;");
45
+ const before = await packageIdentity(run.workspace);
46
+ await Bun.write(source, "export const value = false;");
47
+ const after = await packageIdentity(run.workspace);
48
+ expect(after.digest).not.toBe(before.digest);
49
+ expect(after.files).toBe(before.files);
50
+ } finally { await run.close(); }
51
+ });
52
+
53
+ test("consumer_update_requires_guide_and_pin_without_unrelated_changes", async () => {
54
+ const { validConsumerDiff } = await import("../evals/judgments/comparison/consumer-evidence");
55
+ const guide = ".orly/docs/REST_API_DESIGN_GUIDELINES.md";
56
+ const config = ".orly/orly.json";
57
+ const expected = `${guide}\n${config}\n`;
58
+ expect(validConsumerDiff(expected)).toBe(true);
59
+ for (const changed of ["", guide, config, `${expected}AGENTS.md\n`, `${expected}${config}\n`,
60
+ `${config}\n${guide}\n`, `${guide}\n../.orly/orly.json\n`]) expect(validConsumerDiff(changed)).toBe(false);
61
+ });
62
+
63
+ test("consumer_report_refuses_to_overwrite_prior_attempts", async () => {
64
+ const run = await EvaluationRun.create();
65
+ const report = join(run.workspace, "retained.json");
66
+ const retained = "previous failed attempt";
67
+ try {
68
+ await Bun.write(report, retained);
69
+ const processResult = Bun.spawnSync([process.execPath, join(ROOT, "evals/judgments/comparison/consumer.ts"), run.workspace, run.workspace, report], {
70
+ stdout: "pipe", stderr: "pipe", timeout: TEST_TIMEOUT,
71
+ });
72
+ expect(processResult.exitCode).not.toBe(0);
73
+ expect(processResult.stderr.toString()).toContain("Report must be a new file");
74
+ expect(await Bun.file(report).text()).toBe(retained);
75
+ } finally { await run.close(); }
76
+ });
@@ -0,0 +1,119 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { existsSync, symlinkSync, unlinkSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+
5
+ import { EvaluationRun, EXECUTION_ERROR, TASK_LIMITS, CONSUMER_SUITE_LIMITS } from "../evals/judgments/comparison/execution";
6
+ import { recoverStaleOwnership, OWNER_ERROR } from "../evals/judgments/comparison/recovery";
7
+ import { cleanupTemporaryDirectories, temporaryDirectory } from "./gates_test_support";
8
+
9
+ const RECOVERY = resolve(import.meta.dir, "../evals/judgments/comparison/recovery.ts");
10
+ const OWNER_RECEIPT = "owner.json";
11
+ const SENTINEL_CONTENT = "unrelated owner bytes\n";
12
+ const BUN_EVAL = "-e";
13
+ const TIMEOUT = 10_000;
14
+ const EXCESSIVE_TIMEOUT = 100_000;
15
+ const OUTPUT_LIMIT = 1024;
16
+ const RECOVERY_POLL_MS = 10;
17
+ const SUCCESS_COMMAND = [process.execPath, BUN_EVAL, "process.stdout.write('done')"];
18
+
19
+ afterEach(cleanupTemporaryDirectories);
20
+
21
+ test("restart_recovers_verified_stale_resources_without_completion_credit", async () => {
22
+ const root = temporaryDirectory();
23
+ const marker = join(root, OWNER_RECEIPT);
24
+ const sentinel = join(root, "unrelated.txt");
25
+ await Bun.write(sentinel, SENTINEL_CONTENT);
26
+ const script = `import {createOwnership} from ${JSON.stringify(RECOVERY)};await Bun.write(${JSON.stringify(marker)},JSON.stringify(await createOwnership()));`;
27
+ const owner = Bun.spawn([process.execPath, BUN_EVAL, script], { stdout: "ignore", stderr: "pipe" });
28
+ const timeout = setTimeout(() => owner.kill(), TIMEOUT);
29
+ try {
30
+ expect(await owner.exited).toBe(0);
31
+ const ownership: unknown = await Bun.file(marker).json();
32
+ expect(await recoverStaleOwnership(ownership)).toEqual({ recovered: true, completion: false });
33
+ expect(await Bun.file(sentinel).text()).toBe(SENTINEL_CONTENT);
34
+ await expect(recoverStaleOwnership(ownership)).rejects.toThrow();
35
+ } finally { clearTimeout(timeout); owner.kill(); await owner.exited; }
36
+ }, TIMEOUT);
37
+
38
+ test("foreign_paths_and_symlinked_markers_cannot_authorize_cleanup", async () => {
39
+ const run = await EvaluationRun.create();
40
+ const foreign = temporaryDirectory();
41
+ const sentinel = join(foreign, "unrelated.txt");
42
+ await Bun.write(sentinel, SENTINEL_CONTENT);
43
+ try {
44
+ await expect(recoverStaleOwnership({ ...run.ownership, workspace: foreign })).rejects.toThrow(OWNER_ERROR);
45
+ symlinkSync(foreign, join(run.ownership.receipts, "foreign"));
46
+ expect(await Bun.file(sentinel).text()).toBe(SENTINEL_CONTENT);
47
+ expect(existsSync(run.workspace)).toBe(true);
48
+ } finally { unlinkSync(join(run.ownership.receipts, "foreign")); await run.close(); }
49
+ });
50
+
51
+ test("invalid_limits_refuse_before_starting_any_process", async () => {
52
+ const run = await EvaluationRun.create();
53
+ try {
54
+ for (const timeout_ms of [0, -1, Number.NaN, CONSUMER_SUITE_LIMITS.timeout_ms, EXCESSIVE_TIMEOUT]) {
55
+ await expect(run.execute(SUCCESS_COMMAND, [], { timeout_ms, output_bytes: OUTPUT_LIMIT })).rejects.toThrow(EXECUTION_ERROR);
56
+ }
57
+ } finally { await run.close(); }
58
+ });
59
+
60
+ test("only_the_fixed_consumer_suite_command_receives_the_longer_budget", async () => {
61
+ const run = await EvaluationRun.create();
62
+ try {
63
+ await Bun.write(join(run.workspace, "Makefile"), "test-unit-design-system:\n\t@printf verified\n");
64
+ const suite = await run.executeConsumerSuite(run.workspace, []);
65
+ run.verify(suite);
66
+ expect(suite.command).toEqual(["make", "-C", run.workspace, "test-unit-design-system"]);
67
+ expect(suite.limits).toEqual(CONSUMER_SUITE_LIMITS);
68
+ expect(suite.result.exit_code).toBe(0);
69
+ expect(suite.stdout).toContain("verified");
70
+ expect((await run.execute(SUCCESS_COMMAND, [])).limits).toEqual(TASK_LIMITS);
71
+ } finally { await run.close(); }
72
+ });
73
+
74
+ test("one_owner_refuses_concurrent_execution_and_close_until_work_finishes", async () => {
75
+ const run = await EvaluationRun.create();
76
+ try {
77
+ const pending = run.execute(SUCCESS_COMMAND, []);
78
+ await expect(run.execute(SUCCESS_COMMAND, [])).rejects.toThrow(EXECUTION_ERROR);
79
+ await expect(run.close()).rejects.toThrow(EXECUTION_ERROR);
80
+ expect((await pending).result.exit_code).toBe(0);
81
+ } finally { await run.close(); }
82
+ });
83
+
84
+ test("recovery_refuses_live_descendants_after_the_group_leader_exits", async () => {
85
+ const root = temporaryDirectory();
86
+ const marker = join(root, OWNER_RECEIPT);
87
+ const groupMarker = join(root, "group.txt");
88
+ const script = `import {createOwnership} from ${JSON.stringify(RECOVERY)};
89
+ const owner=await createOwnership();
90
+ const leader=Bun.spawn(['sh','-c','sleep 30 >/dev/null 2>&1 &'],{detached:true,stdout:'ignore',stderr:'ignore'});
91
+ await Bun.write(owner.receipts+'/command/child.pid',String(leader.pid));
92
+ await Bun.write(${JSON.stringify(groupMarker)},String(leader.pid));
93
+ await leader.exited;
94
+ await Bun.write(${JSON.stringify(marker)},JSON.stringify(owner));`;
95
+ const owner = Bun.spawn([process.execPath, BUN_EVAL, script], { stdout: "ignore", stderr: "pipe" });
96
+ const timeout = setTimeout(() => owner.kill(), TIMEOUT);
97
+ try {
98
+ expect(await owner.exited).toBe(0);
99
+ const ownership: unknown = await Bun.file(marker).json();
100
+ const group = Number(await Bun.file(groupMarker).text());
101
+ process.kill(-group, 0);
102
+ await expect(recoverStaleOwnership(ownership)).rejects.toThrow(OWNER_ERROR);
103
+ } finally {
104
+ clearTimeout(timeout);
105
+ owner.kill();
106
+ await owner.exited;
107
+ const group = Number(await Bun.file(groupMarker).text());
108
+ try { process.kill(-group, "SIGKILL"); } catch { /* Already exited. */ }
109
+ const ownership: unknown = await Bun.file(marker).json();
110
+ const deadline = Date.now() + TIMEOUT;
111
+ while (true) {
112
+ try { await recoverStaleOwnership(ownership); break; }
113
+ catch (error) {
114
+ if (Date.now() >= deadline) throw error;
115
+ await Bun.sleep(RECOVERY_POLL_MS);
116
+ }
117
+ }
118
+ }
119
+ }, TIMEOUT * 2);
@@ -1,4 +1,4 @@
1
- import type { Prepared } from "./types";
1
+ import type { ProviderInput } from "./types";
2
2
 
3
3
  import { OrlyError } from "../model";
4
4
  import { ENDPOINT, MAX_RESPONSE_BYTES, REQUEST_TIMEOUT_MS } from "./constants";
@@ -7,7 +7,7 @@ import { parseReply, type Reply } from "./wire";
7
7
 
8
8
  export type Fetch = (request: Request) => Promise<Response>;
9
9
 
10
- export async function requestAdvice(prepared: Prepared, credential: string, fetcher: Fetch = fetch, timeoutMs = REQUEST_TIMEOUT_MS): Promise<Reply> {
10
+ export async function requestAdvice(prepared: ProviderInput, credential: string, fetcher: Fetch = fetch, timeoutMs = REQUEST_TIMEOUT_MS): Promise<Reply> {
11
11
  const controller = new AbortController();
12
12
  const timer = setTimeout(() => controller.abort(), timeoutMs);
13
13
  let response: Response | undefined;
@@ -34,6 +34,7 @@ export type QuestionId = Item["question"];
34
34
  export type ProviderQuestion =
35
35
  | { type: "noul"; instructions: string; criteria: { true: string; false: string } }
36
36
  | { type: "choice"; instructions: string; criteria: Record<string, string> };
37
+ export type ProviderInput = { request: string; definition: { question: ProviderQuestion } };
37
38
  export type Definition = {
38
39
  stage: Stage;
39
40
  requiredRoles: readonly Role[];
@@ -1,4 +1,4 @@
1
- import type { Prepared, ProviderQuestion } from "./types";
1
+ import type { ProviderInput, ProviderQuestion } from "./types";
2
2
 
3
3
  import { z } from "zod";
4
4
 
@@ -15,7 +15,7 @@ export const replySchema = z.object({ model: z.literal(MODEL), answers: z.record
15
15
  export type Reply = z.infer<typeof replySchema>;
16
16
  export type Answer = z.infer<typeof answerSchema>;
17
17
 
18
- export function parseReply(value: unknown, prepared: Prepared): Reply {
18
+ export function parseReply(value: unknown, prepared: ProviderInput): Reply {
19
19
  const parsed = replySchema.safeParse(value);
20
20
  if (!parsed.success) throw new OrlyError("Provider reply does not match the pinned model and answer schema.");
21
21
  const reply = parsed.data;
@@ -30,7 +30,11 @@ else if(args[0]==="pr" && args[1]==="view") {
30
30
  else { const prior=(await Bun.file(process.env.POLL_CALLS).text()).split("\\n").filter(x=>x.includes("headRefOid")).length; console.log((process.env.POLL_MODE==="changed" && prior>1) || (process.env.POLL_MODE==="changed-check" && prior>2) ? "changed-revision" : process.env.POLL_HEAD); }
31
31
  } else if(args[0]==="pr" && args[1]==="checks") {
32
32
  if(process.env.POLL_STATE==="fetch-failed"){console.error("temporary fetch refusal");process.exit(2);}
33
- console.log(JSON.stringify(process.env.POLL_STATE==="empty" ? [] : [{name:"tests",state:"SUCCESS",bucket:process.env.POLL_STATE==="failed" ? "fail" : "pass",link:"https://example.invalid/check"}]));
33
+ const state=process.env.POLL_STATE;
34
+ const bucket=state==="failed" ? "fail" : state==="pending" ? "pending" : "pass";
35
+ console.log(JSON.stringify(state==="empty" ? [] : state==="malformed-check" ? [{name:"tests"}] : [{name:"tests",state:bucket==="pass" ? "SUCCESS" : bucket==="fail" ? "FAILURE" : "IN_PROGRESS",bucket,link:"https://example.invalid/check"}]));
36
+ if(state==="failed") process.exit(1);
37
+ if(state==="pending") process.exit(8);
34
38
  } else if(args[0]==="api") {
35
39
  if(process.env.POLL_MODE==="fetch-failed" && args[1].includes("/issues/")){console.error("temporary summary refusal");process.exit(2);}
36
40
  const revision=process.env.POLL_MODE==="stale" ? "old-revision" : process.env.POLL_HEAD;
@@ -75,7 +79,7 @@ for (const mode of ["fetch-failed", "malformed", "changed", "changed-check", "st
75
79
  });
76
80
  }
77
81
 
78
- for (const state of ["empty", "fetch-failed", "failed"]) {
82
+ for (const state of ["empty", "fetch-failed", "failed", "pending", "malformed-check"]) {
79
83
  test(`${state} check discovery stays incomplete`, () => {
80
84
  const result = poll("complete", state);
81
85
  expect(result.code).not.toBe(0);
@@ -84,6 +88,17 @@ for (const state of ["empty", "fetch-failed", "failed"]) {
84
88
  });
85
89
  }
86
90
 
91
+ for (const state of ["failed", "pending"]) {
92
+ test(`${state} GitHub checks retain job details despite nonzero status`, () => {
93
+ const result = poll("complete", state);
94
+ expect(result.code).not.toBe(0);
95
+ expect(result.output).toContain("tests |");
96
+ expect(result.output).toContain("https://example.invalid/check");
97
+ expect(result.output).toContain(state === "failed" ? "FAILURE | fail" : "IN_PROGRESS | pending");
98
+ expect(result.output).not.toContain("check fetch incomplete");
99
+ });
100
+ }
101
+
87
102
  function gitlabPoll(mode = "complete", state = "green") {
88
103
  const root = newRepository();
89
104
  const tools = join(root, "tools");
@@ -105,7 +120,7 @@ else if(args[0]==="mr") {
105
120
  if(process.env.POLL_MODE==="fetch-failed" && endpoint.endsWith("/notes")){console.error("temporary summary refusal");process.exit(2);}
106
121
  if(endpoint.endsWith("/pipelines")) {
107
122
  if(process.env.POLL_STATE==="fetch-failed"){console.error("temporary pipeline refusal");process.exit(2);}
108
- console.log(JSON.stringify(process.env.POLL_STATE==="empty" ? [] : [{id:3,sha:process.env.POLL_HEAD,status:process.env.POLL_STATE==="failed" ? "failed" : "success"}]));
123
+ console.log(JSON.stringify(process.env.POLL_STATE==="empty" ? [] : [{id:3,sha:process.env.POLL_HEAD,status:process.env.POLL_STATE==="failed" ? "failed" : process.env.POLL_STATE==="pending" ? "running" : "success",web_url:"https://example.invalid/pipeline"}]));
109
124
  } else if(process.env.POLL_MODE==="malformed") console.log(JSON.stringify({message:"not review evidence"}));
110
125
  else {
111
126
  const note=id=>({id,author:{username:"greptile"},body:id===1 ? "first-page finding" : "second-page finding",type:null});
@@ -148,7 +163,7 @@ for (const mode of ["fetch-failed", "malformed", "changed", "changed-check"]) {
148
163
  });
149
164
  }
150
165
 
151
- for (const state of ["empty", "fetch-failed", "failed"]) {
166
+ for (const state of ["empty", "fetch-failed", "failed", "pending"]) {
152
167
  test(`GitLab ${state} pipeline discovery stays incomplete`, () => {
153
168
  const result = gitlabPoll("complete", state);
154
169
  expect(result.code).not.toBe(0);
@@ -156,3 +171,12 @@ for (const state of ["empty", "fetch-failed", "failed"]) {
156
171
  expect(result.output).not.toContain("ci=green");
157
172
  });
158
173
  }
174
+
175
+ for (const state of ["failed", "pending"]) {
176
+ test(`GitLab ${state} pipeline retains its state and link`, () => {
177
+ const result = gitlabPoll("complete", state);
178
+ expect(result.code).not.toBe(0);
179
+ expect(result.output).toContain(`pipeline 3 | ${state === "failed" ? "failed" : "running"}`);
180
+ expect(result.output).toContain("https://example.invalid/pipeline");
181
+ });
182
+ }