self-bench 0.3.0 → 0.3.2
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/.dockerignore +10 -0
- package/Dockerfile +37 -0
- package/Dockerfile.sandbox +24 -0
- package/README.md +34 -26
- package/biome.json +18 -0
- package/bun.lock +1182 -0
- package/compose.yaml +85 -0
- package/dist/agent-smoke-main.js +1 -1
- package/dist/build-metadata.d.ts +2 -0
- package/dist/build-metadata.d.ts.map +1 -0
- package/dist/build-metadata.js +2 -0
- package/dist/build-metadata.js.map +1 -0
- package/dist/cli.js +18 -8
- package/dist/cli.js.map +1 -1
- package/dist/eval-main.js +1 -1
- package/dist/reaudit-main.js +1 -1
- package/dist/repair-main.js +1 -1
- package/dist/validate-main.js +1 -1
- package/docs/evaluations.md +67 -0
- package/docs/operations.md +178 -0
- package/docs/task-construction.md +94 -0
- package/package.json +28 -15
- package/scripts/verify-package.ts +57 -0
- package/scripts/write-build-metadata.ts +27 -0
- package/src/activities.ts +1236 -0
- package/src/agent-smoke-main.ts +63 -0
- package/src/agent-smoke.ts +132 -0
- package/src/api-main.ts +12 -0
- package/src/api.ts +239 -0
- package/src/artifacts.ts +361 -0
- package/src/audit.ts +106 -0
- package/src/build-metadata.ts +3 -0
- package/src/cli.ts +350 -0
- package/src/codex-review.ts +220 -0
- package/src/config.ts +117 -0
- package/src/contracts.ts +209 -0
- package/src/coupling.ts +259 -0
- package/src/docker-executor.ts +115 -0
- package/src/eval-main.ts +92 -0
- package/src/evaluate.ts +293 -0
- package/src/github.ts +26 -0
- package/src/harbor-results.ts +142 -0
- package/src/harbor-task.ts +528 -0
- package/src/hash.ts +5 -0
- package/src/modal-auth.ts +11 -0
- package/src/modal-executor.ts +176 -0
- package/src/parallel.ts +24 -0
- package/src/process.ts +165 -0
- package/src/provenance.ts +458 -0
- package/src/reaudit-main.ts +192 -0
- package/src/repair-main.ts +203 -0
- package/src/repair.ts +55 -0
- package/src/run-wait.ts +40 -0
- package/src/sandbox-author.ts +19 -0
- package/src/sandbox-repair.ts +160 -0
- package/src/sandbox-review.ts +17 -0
- package/src/sandbox-validation-repair.ts +174 -0
- package/src/sandbox.ts +51 -0
- package/src/subscription-auth.ts +67 -0
- package/src/temporal.ts +23 -0
- package/src/validate-main.ts +171 -0
- package/src/validation-repair.ts +94 -0
- package/src/worker-main.ts +32 -0
- package/src/workflow.ts +519 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +21 -0
package/src/evaluate.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { cp, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { archiveIncompleteHarborJob, tryReadHarborJobResult } from "./harbor-results.js";
|
|
5
|
+
import { parallelMap } from "./parallel.js";
|
|
6
|
+
import { runCommand } from "./process.js";
|
|
7
|
+
import { assertCodexSubscriptionAuth } from "./subscription-auth.js";
|
|
8
|
+
|
|
9
|
+
export const MATRIX_MODELS = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] as const;
|
|
10
|
+
export type MatrixModel = (typeof MATRIX_MODELS)[number];
|
|
11
|
+
const CODEX_VERSION = "0.146.1";
|
|
12
|
+
|
|
13
|
+
export interface MatrixOptions {
|
|
14
|
+
readonly exportPath?: string;
|
|
15
|
+
readonly tasksPath?: string;
|
|
16
|
+
readonly jobsDirectory: string;
|
|
17
|
+
readonly harborPath?: string;
|
|
18
|
+
readonly environment?: "docker" | "modal";
|
|
19
|
+
readonly concurrency?: number;
|
|
20
|
+
readonly authPath?: string;
|
|
21
|
+
readonly models?: readonly MatrixModel[];
|
|
22
|
+
readonly onTrialComplete?: (
|
|
23
|
+
summary: MatrixTrialSummary,
|
|
24
|
+
completed: number,
|
|
25
|
+
total: number,
|
|
26
|
+
) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MatrixTrialSummary {
|
|
30
|
+
readonly taskId: string;
|
|
31
|
+
readonly model: MatrixModel;
|
|
32
|
+
readonly jobName: string;
|
|
33
|
+
readonly passed: boolean;
|
|
34
|
+
readonly rewards: Readonly<Record<string, number>>;
|
|
35
|
+
readonly exception?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runMatrix(options: MatrixOptions): Promise<readonly MatrixTrialSummary[]> {
|
|
39
|
+
const root = resolve(options.jobsDirectory);
|
|
40
|
+
const tasksDirectory = join(root, "tasks");
|
|
41
|
+
await mkdir(root, { recursive: true });
|
|
42
|
+
const taskDirectories = await resolveMatrixTasks(options, tasksDirectory);
|
|
43
|
+
const authPath = resolve(options.authPath ?? join(homedir(), ".codex/auth.json"));
|
|
44
|
+
await assertSubscriptionAuth(authPath);
|
|
45
|
+
const models = options.models ?? MATRIX_MODELS;
|
|
46
|
+
if (models.length < 1) {
|
|
47
|
+
throw new Error("provide at least one evaluation model");
|
|
48
|
+
}
|
|
49
|
+
const work = models.flatMap((model) =>
|
|
50
|
+
taskDirectories.map((taskDirectory) => ({ model, taskDirectory })),
|
|
51
|
+
);
|
|
52
|
+
let completed = 0;
|
|
53
|
+
const summaries = await parallelMap(work, options.concurrency ?? 3, async (item) => {
|
|
54
|
+
const summary = await runTrial({
|
|
55
|
+
jobsDirectory: root,
|
|
56
|
+
harborPath: options.harborPath ?? "harbor",
|
|
57
|
+
environment: options.environment ?? "modal",
|
|
58
|
+
authPath,
|
|
59
|
+
...item,
|
|
60
|
+
});
|
|
61
|
+
completed += 1;
|
|
62
|
+
options.onTrialComplete?.(summary, completed, work.length);
|
|
63
|
+
return summary;
|
|
64
|
+
});
|
|
65
|
+
await writeFile(
|
|
66
|
+
join(root, "summary.json"),
|
|
67
|
+
`${JSON.stringify(
|
|
68
|
+
{
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
agent: "codex",
|
|
71
|
+
agentVersion: CODEX_VERSION,
|
|
72
|
+
auth: "codex-subscription",
|
|
73
|
+
reasoningEffort: "high",
|
|
74
|
+
models,
|
|
75
|
+
taskCount: taskDirectories.length,
|
|
76
|
+
trials: summaries,
|
|
77
|
+
},
|
|
78
|
+
null,
|
|
79
|
+
2,
|
|
80
|
+
)}\n`,
|
|
81
|
+
);
|
|
82
|
+
return summaries;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function resolveMatrixTasks(
|
|
86
|
+
options: MatrixOptions,
|
|
87
|
+
materializedTasksDirectory: string,
|
|
88
|
+
): Promise<string[]> {
|
|
89
|
+
if (options.exportPath && options.tasksPath) {
|
|
90
|
+
throw new Error("provide exactly one of exportPath or tasksPath");
|
|
91
|
+
}
|
|
92
|
+
if (options.tasksPath) {
|
|
93
|
+
const tasks = await taskDirectories(resolve(options.tasksPath));
|
|
94
|
+
if (tasks.length < 1) {
|
|
95
|
+
throw new Error(`found no Harbor tasks in ${resolve(options.tasksPath)}`);
|
|
96
|
+
}
|
|
97
|
+
return tasks;
|
|
98
|
+
}
|
|
99
|
+
if (!options.exportPath) {
|
|
100
|
+
throw new Error("provide exactly one of exportPath or tasksPath");
|
|
101
|
+
}
|
|
102
|
+
const tasks = await materializeExport(resolve(options.exportPath), materializedTasksDirectory);
|
|
103
|
+
if (tasks.length < 1) {
|
|
104
|
+
throw new Error(`found no Harbor tasks in ${resolve(options.exportPath)}`);
|
|
105
|
+
}
|
|
106
|
+
return tasks;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function runTrial(input: {
|
|
110
|
+
readonly taskDirectory: string;
|
|
111
|
+
readonly model: MatrixModel;
|
|
112
|
+
readonly jobsDirectory: string;
|
|
113
|
+
readonly harborPath: string;
|
|
114
|
+
readonly environment: "docker" | "modal";
|
|
115
|
+
readonly authPath: string;
|
|
116
|
+
}): Promise<MatrixTrialSummary> {
|
|
117
|
+
const taskId = basename(input.taskDirectory);
|
|
118
|
+
const jobName = `${taskId}-${input.model}`.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
|
|
119
|
+
const existing = await tryReadHarborJobResult(input.jobsDirectory, jobName);
|
|
120
|
+
if (existing) {
|
|
121
|
+
return summarizeResult(taskId, input.model, jobName, existing.trial);
|
|
122
|
+
}
|
|
123
|
+
await archiveIncompleteHarborJob(input.jobsDirectory, jobName);
|
|
124
|
+
|
|
125
|
+
const environment = { ...process.env };
|
|
126
|
+
delete environment.OPENAI_API_KEY;
|
|
127
|
+
environment.CODEX_FORCE_AUTH_JSON = "1";
|
|
128
|
+
environment.CODEX_AUTH_JSON_PATH = input.authPath;
|
|
129
|
+
let result: Awaited<ReturnType<typeof runCommand>>;
|
|
130
|
+
try {
|
|
131
|
+
result = await runCommand(
|
|
132
|
+
input.harborPath,
|
|
133
|
+
[
|
|
134
|
+
"run",
|
|
135
|
+
"--path",
|
|
136
|
+
input.taskDirectory,
|
|
137
|
+
"--agent",
|
|
138
|
+
"codex",
|
|
139
|
+
"--model",
|
|
140
|
+
input.model,
|
|
141
|
+
"--ak",
|
|
142
|
+
`version=${CODEX_VERSION}`,
|
|
143
|
+
"--ak",
|
|
144
|
+
"reasoning_effort=high",
|
|
145
|
+
"--env",
|
|
146
|
+
input.environment,
|
|
147
|
+
"--job-name",
|
|
148
|
+
jobName,
|
|
149
|
+
"--jobs-dir",
|
|
150
|
+
input.jobsDirectory,
|
|
151
|
+
"--n-concurrent",
|
|
152
|
+
"1",
|
|
153
|
+
"--max-retries",
|
|
154
|
+
"1",
|
|
155
|
+
"--delete",
|
|
156
|
+
"--yes",
|
|
157
|
+
"--quiet",
|
|
158
|
+
],
|
|
159
|
+
{ allowFailure: true, env: environment, timeoutMs: 4 * 60 * 60 * 1000 },
|
|
160
|
+
);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return failedTrial(taskId, input.model, jobName, errorMessage(error));
|
|
163
|
+
}
|
|
164
|
+
const completed = await tryReadHarborJobResult(input.jobsDirectory, jobName);
|
|
165
|
+
if (!completed) {
|
|
166
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
167
|
+
return failedTrial(
|
|
168
|
+
taskId,
|
|
169
|
+
input.model,
|
|
170
|
+
jobName,
|
|
171
|
+
`Harbor produced no result (exit ${result.exitCode})${detail ? `: ${detail.slice(-1000)}` : ""}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return summarizeResult(taskId, input.model, jobName, completed.trial);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function summarizeResult(
|
|
178
|
+
taskId: string,
|
|
179
|
+
model: MatrixModel,
|
|
180
|
+
jobName: string,
|
|
181
|
+
result: unknown,
|
|
182
|
+
): MatrixTrialSummary {
|
|
183
|
+
const trial =
|
|
184
|
+
isRecord(result) && Array.isArray(result.trial_results) ? result.trial_results[0] : result;
|
|
185
|
+
if (!isRecord(trial)) {
|
|
186
|
+
throw new Error(`invalid Harbor result for ${taskId}/${model}`);
|
|
187
|
+
}
|
|
188
|
+
const rawRewards =
|
|
189
|
+
isRecord(trial.verifier_result) && isRecord(trial.verifier_result.rewards)
|
|
190
|
+
? trial.verifier_result.rewards
|
|
191
|
+
: {};
|
|
192
|
+
const rewards = Object.fromEntries(
|
|
193
|
+
Object.entries(rawRewards).flatMap(([key, value]) =>
|
|
194
|
+
typeof value === "number" ? [[key, value] as const] : [],
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
const exception = exceptionText(trial.exception_info ?? trial.exception);
|
|
198
|
+
return {
|
|
199
|
+
taskId,
|
|
200
|
+
model,
|
|
201
|
+
jobName,
|
|
202
|
+
passed:
|
|
203
|
+
!exception &&
|
|
204
|
+
Object.keys(rewards).length > 0 &&
|
|
205
|
+
(rewards.reward === undefined
|
|
206
|
+
? Object.values(rewards).every((reward) => reward >= 1)
|
|
207
|
+
: rewards.reward >= 1),
|
|
208
|
+
rewards,
|
|
209
|
+
...(exception ? { exception } : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function failedTrial(
|
|
214
|
+
taskId: string,
|
|
215
|
+
model: MatrixModel,
|
|
216
|
+
jobName: string,
|
|
217
|
+
exception: string,
|
|
218
|
+
): MatrixTrialSummary {
|
|
219
|
+
return { taskId, model, jobName, passed: false, rewards: {}, exception };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function errorMessage(error: unknown): string {
|
|
223
|
+
return error instanceof Error ? error.message : String(error);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function materializeExport(exportPath: string, tasksDirectory: string): Promise<string[]> {
|
|
227
|
+
const existing = await taskDirectories(tasksDirectory);
|
|
228
|
+
if (existing.length > 0) {
|
|
229
|
+
return existing;
|
|
230
|
+
}
|
|
231
|
+
return await withTemporaryDirectory("selfbench-matrix-", async (temporary) => {
|
|
232
|
+
await runCommand("tar", ["-xzf", exportPath, "-C", temporary]);
|
|
233
|
+
const archivesRoot = join(temporary, "tasks");
|
|
234
|
+
const archives = (await readdir(archivesRoot))
|
|
235
|
+
.filter((name) => name.endsWith(".tar.gz"))
|
|
236
|
+
.sort();
|
|
237
|
+
await mkdir(tasksDirectory, { recursive: true });
|
|
238
|
+
for (const archive of archives) {
|
|
239
|
+
const taskId = archive.slice(0, -".tar.gz".length);
|
|
240
|
+
const expanded = join(temporary, `expanded-${taskId}`);
|
|
241
|
+
await mkdir(expanded);
|
|
242
|
+
await runCommand("tar", ["-xzf", join(archivesRoot, archive), "-C", expanded]);
|
|
243
|
+
await cp(join(expanded, "harbor-task"), join(tasksDirectory, taskId), { recursive: true });
|
|
244
|
+
}
|
|
245
|
+
return await taskDirectories(tasksDirectory);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function taskDirectories(root: string): Promise<string[]> {
|
|
250
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
251
|
+
const paths: string[] = [];
|
|
252
|
+
for (const entry of entries) {
|
|
253
|
+
if (!entry.isDirectory()) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const path = join(root, entry.name);
|
|
257
|
+
if (await readFile(join(path, "task.toml")).catch(() => undefined)) {
|
|
258
|
+
paths.push(path);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return paths.sort();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function assertSubscriptionAuth(path: string): Promise<void> {
|
|
265
|
+
const parsed = JSON.parse(await readFile(path, "utf8")) as unknown;
|
|
266
|
+
assertCodexSubscriptionAuth(parsed, path);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function withTemporaryDirectory<T>(
|
|
270
|
+
prefix: string,
|
|
271
|
+
action: (root: string) => Promise<T>,
|
|
272
|
+
): Promise<T> {
|
|
273
|
+
const root = await mkdtemp(join(tmpdir(), prefix));
|
|
274
|
+
try {
|
|
275
|
+
return await action(root);
|
|
276
|
+
} finally {
|
|
277
|
+
await rm(root, { recursive: true, force: true });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function exceptionText(value: unknown): string | undefined {
|
|
282
|
+
if (typeof value === "string" && value) {
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
if (isRecord(value)) {
|
|
286
|
+
return typeof value.message === "string" ? value.message : JSON.stringify(value);
|
|
287
|
+
}
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
292
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
293
|
+
}
|
package/src/github.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function assertPullRequestBelongsToRepository(
|
|
2
|
+
repositoryUrl: string,
|
|
3
|
+
pullRequestUrl: string,
|
|
4
|
+
pullRequestNumber: number,
|
|
5
|
+
): void {
|
|
6
|
+
const repository = githubRepository(repositoryUrl);
|
|
7
|
+
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(pullRequestUrl);
|
|
8
|
+
if (!match?.[1] || !match[2] || !match[3]) {
|
|
9
|
+
throw new Error(`invalid GitHub pull request URL: ${pullRequestUrl}`);
|
|
10
|
+
}
|
|
11
|
+
const candidateRepository = `${match[1]}/${match[2]}`.toLowerCase();
|
|
12
|
+
if (candidateRepository !== repository || Number(match[3]) !== pullRequestNumber) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`pull request ${pullRequestUrl} does not match ${repository}#${pullRequestNumber}`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function githubRepository(url: string): string {
|
|
20
|
+
const match =
|
|
21
|
+
/^(?:https:\/\/github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(?:\.git)?\/?$/i.exec(url);
|
|
22
|
+
if (!match?.[1] || !match[2]) {
|
|
23
|
+
throw new Error(`unsupported GitHub repository URL: ${url}`);
|
|
24
|
+
}
|
|
25
|
+
return `${match[1]}/${match[2]}`.toLowerCase();
|
|
26
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readdir, readFile, rename } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface HarborVerifierOutput {
|
|
5
|
+
readonly combined?: string;
|
|
6
|
+
readonly stderr?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface HarborJobResult {
|
|
10
|
+
readonly job: unknown;
|
|
11
|
+
readonly trial: unknown;
|
|
12
|
+
readonly verifier?: HarborVerifierOutput;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const infrastructurePatterns = [
|
|
16
|
+
/unknown flag: --project-name/i,
|
|
17
|
+
/cannot connect to the docker daemon/i,
|
|
18
|
+
/error during connect/i,
|
|
19
|
+
/connection refused/i,
|
|
20
|
+
/modal.*(?:unavailable|timed out|timeout)/i,
|
|
21
|
+
/image build for im-[a-z0-9]+ failed/i,
|
|
22
|
+
/all predefined address pools have been fully subnetted/i,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
class IncompleteHarborJobError extends Error {}
|
|
26
|
+
|
|
27
|
+
export function harborInfrastructureError(trial: unknown): string | undefined {
|
|
28
|
+
if (!isRecord(trial) || !isRecord(trial.exception_info)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const message = trial.exception_info.exception_message;
|
|
32
|
+
const type = trial.exception_info.exception_type;
|
|
33
|
+
if (
|
|
34
|
+
typeof message !== "string" ||
|
|
35
|
+
(type !== "AuthError" && !infrastructurePatterns.some((pattern) => pattern.test(message)))
|
|
36
|
+
) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return `${typeof type === "string" ? type : "HarborError"}: ${message}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function readHarborJobResult(
|
|
43
|
+
jobsDirectory: string,
|
|
44
|
+
jobName: string,
|
|
45
|
+
): Promise<HarborJobResult> {
|
|
46
|
+
const jobDirectory = join(jobsDirectory, jobName);
|
|
47
|
+
const job = JSON.parse(await readFile(join(jobDirectory, "result.json"), "utf8")) as unknown;
|
|
48
|
+
const entries = await readdir(jobDirectory, { withFileTypes: true });
|
|
49
|
+
const trials: Array<{ directory: string; result: unknown }> = [];
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.isDirectory()) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const directory = join(jobDirectory, entry.name);
|
|
55
|
+
const raw = await readFile(join(directory, "result.json"), "utf8").catch((error: unknown) =>
|
|
56
|
+
isNotFound(error) ? undefined : Promise.reject(error),
|
|
57
|
+
);
|
|
58
|
+
if (raw) {
|
|
59
|
+
trials.push({ directory, result: JSON.parse(raw) as unknown });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const aggregateTrials =
|
|
63
|
+
isRecord(job) && Array.isArray(job.trial_results) ? job.trial_results : [];
|
|
64
|
+
const [onlyTrial] = trials;
|
|
65
|
+
if (onlyTrial && trials.length === 1) {
|
|
66
|
+
return {
|
|
67
|
+
job,
|
|
68
|
+
trial: onlyTrial.result,
|
|
69
|
+
...(await readVerifierOutput(onlyTrial.directory)),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (aggregateTrials.length === 1) {
|
|
73
|
+
return { job, trial: aggregateTrials[0] };
|
|
74
|
+
}
|
|
75
|
+
if (trials.length === 0 && isRecord(job) && job.finished_at === null) {
|
|
76
|
+
throw new IncompleteHarborJobError(`Harbor job ${jobName} has not finished`);
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`expected one Harbor trial result in ${jobDirectory}, found ${trials.length}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function tryReadHarborJobResult(
|
|
82
|
+
jobsDirectory: string,
|
|
83
|
+
jobName: string,
|
|
84
|
+
): Promise<HarborJobResult | undefined> {
|
|
85
|
+
try {
|
|
86
|
+
return await readHarborJobResult(jobsDirectory, jobName);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (isNotFound(error) || error instanceof IncompleteHarborJobError) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function archiveIncompleteHarborJob(
|
|
96
|
+
jobsDirectory: string,
|
|
97
|
+
jobName: string,
|
|
98
|
+
): Promise<string | undefined> {
|
|
99
|
+
const source = join(jobsDirectory, jobName);
|
|
100
|
+
const destination = `${source}.incomplete-${Date.now()}`;
|
|
101
|
+
try {
|
|
102
|
+
await rename(source, destination);
|
|
103
|
+
return destination;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (isNotFound(error)) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function readVerifierOutput(
|
|
113
|
+
trialDirectory: string,
|
|
114
|
+
): Promise<{ readonly verifier?: HarborVerifierOutput }> {
|
|
115
|
+
const verifierDirectory = join(trialDirectory, "verifier");
|
|
116
|
+
const [combined, stderr] = await Promise.all([
|
|
117
|
+
readOptionalText(join(verifierDirectory, "test-stdout.txt")),
|
|
118
|
+
readOptionalText(join(verifierDirectory, "test-stderr.txt")),
|
|
119
|
+
]);
|
|
120
|
+
return combined || stderr
|
|
121
|
+
? { verifier: { ...(combined ? { combined } : {}), ...(stderr ? { stderr } : {}) } }
|
|
122
|
+
: {};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readOptionalText(path: string): Promise<string | undefined> {
|
|
126
|
+
return await readFile(path, "utf8").catch((error: unknown) =>
|
|
127
|
+
isNotFound(error) ? undefined : Promise.reject(error),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
132
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isNotFound(error: unknown): boolean {
|
|
136
|
+
return (
|
|
137
|
+
typeof error === "object" &&
|
|
138
|
+
error !== null &&
|
|
139
|
+
"code" in error &&
|
|
140
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
141
|
+
);
|
|
142
|
+
}
|