@tangle-network/agent-bench 0.1.0 → 0.3.5
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/CHANGELOG.md +17 -0
- package/HARNESS.md +302 -0
- package/README.md +26 -1
- package/fixtures/aec-bench.json +18 -0
- package/fixtures/agentbench-dbbench.json +22 -0
- package/fixtures/bfcl.json +45 -0
- package/fixtures/commit0.json +72 -0
- package/fixtures/crag.json +10 -0
- package/fixtures/dabstep.json +22 -0
- package/fixtures/enterpriseops-gym.json +103 -0
- package/fixtures/finresearchbench.json +21 -0
- package/fixtures/finsearchcomp.json +66 -0
- package/fixtures/frames.json +26 -0
- package/fixtures/hotpotqa.json +182 -0
- package/fixtures/nomiracl.json +26 -0
- package/fixtures/open-rag-bench.json +16 -0
- package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
- package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
- package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
- package/fixtures/pier-agent/no-model-task/task.toml +35 -0
- package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
- package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
- package/fixtures/programbench.json +17 -0
- package/fixtures/ragbench.json +21 -0
- package/fixtures/simpleqa.json +121 -0
- package/fixtures/t2-ragbench.json +13 -0
- package/fixtures/tau2-bench.json +16 -0
- package/fixtures/tau3-banking.json +16 -0
- package/fixtures/toollm.json +28 -0
- package/fixtures/webarena-verified.json +20 -0
- package/package.json +39 -15
- package/pier_agents/__init__.py +18 -0
- package/pier_agents/candidate_contract.py +755 -0
- package/pier_agents/process_boundary.py +321 -0
- package/pier_agents/tangle_candidate.py +907 -0
- package/pier_agents/workspace_boundary.py +368 -0
- package/scripts/appworld_driver.py +359 -0
- package/scripts/cadbench_prepare.py +22 -0
- package/scripts/cadgenbench_hard_parts.py +48 -0
- package/scripts/clbench_codebase_judge.py +73 -0
- package/scripts/commit0_judge.py +170 -0
- package/scripts/dabstep_judge.py +42 -0
- package/scripts/enterpriseops_gym_judge.py +281 -0
- package/scripts/programbench_judge.py +120 -0
- package/scripts/render-gate-chart.mjs +176 -0
- package/scripts/run-package-tests.mjs +56 -0
- package/scripts/terminate-pier-trial.mts +66 -0
- package/scripts/trata-hedge/README.md +56 -0
- package/scripts/trata-hedge/run.sh +60 -0
- package/scripts/trata-hedge/solve.py +83 -0
- package/scripts/verify-packed-consumer.mjs +224 -0
- package/scripts/verify-pier-agent.mts +715 -0
- package/scripts/verify-pier-pair.mts +74 -0
- package/scripts/verify-pier-recovery.mts +139 -0
- package/src/adapters.ts +26 -0
- package/src/benchmarks/_harness.test.mts +178 -0
- package/src/benchmarks/_harness.ts +239 -16
- package/src/benchmarks/agentbench.ts +163 -0
- package/src/benchmarks/appworld.test.mts +15 -9
- package/src/benchmarks/bfcl.ts +346 -0
- package/src/benchmarks/crag.ts +137 -0
- package/src/benchmarks/dabstep.test.mts +70 -0
- package/src/benchmarks/dabstep.ts +212 -0
- package/src/benchmarks/external-adapters.test.mts +150 -0
- package/src/benchmarks/finresearchbench.ts +269 -0
- package/src/benchmarks/humaneval.ts +20 -8
- package/src/benchmarks/nomiracl.ts +180 -0
- package/src/benchmarks/open-rag-bench.ts +153 -0
- package/src/benchmarks/rag-benchmarks.test.mts +138 -0
- package/src/benchmarks/rag-shared.ts +327 -0
- package/src/benchmarks/ragbench.ts +171 -0
- package/src/benchmarks/swe-bench.test.mts +61 -0
- package/src/benchmarks/swe-bench.ts +201 -19
- package/src/benchmarks/t2-ragbench.ts +166 -0
- package/src/benchmarks/tau-bench-shared.ts +214 -0
- package/src/benchmarks/tau2-bench.ts +30 -0
- package/src/benchmarks/tau3-banking.ts +29 -0
- package/src/benchmarks/terminal-bench.test.mts +33 -0
- package/src/benchmarks/terminal-bench.ts +23 -8
- package/src/benchmarks/toollm.ts +254 -0
- package/src/benchmarks/types.ts +42 -0
- package/src/benchmarks/webarena-verified.ts +200 -0
- package/src/commit0-prereqs.sh +0 -0
- package/src/coordination-mcp-container-reach.mts +181 -0
- package/src/decoder-live.mts +1 -1
- package/src/examples/README.md +103 -39
- package/src/examples/benchmark-matrix.mts +101 -0
- package/src/examples/lean-proof-gate.README.md +77 -0
- package/src/examples/lean-proof-gate.mts +162 -0
- package/src/examples/lean-verify.ts +95 -0
- package/src/examples/lean.Dockerfile +12 -0
- package/src/examples/math-demo.mts +9 -7
- package/src/examples/strategy-demo.mts +10 -12
- package/src/gate.ts +3 -2
- package/src/hev-eval.mts +69 -0
- package/src/hev-improve.mts +169 -0
- package/src/hev-structural.mts +688 -0
- package/src/index.ts +73 -0
- package/src/mbpp-structural.mts +662 -0
- package/src/pier-agent.test-fixtures.mts +19 -0
- package/src/pier-agent.test.mts +363 -0
- package/src/pier-agent.ts +657 -0
- package/src/pier-result-grader.mjs +30 -0
- package/src/pier-result-grader.test.mts +62 -0
- package/src/pier-result-grader.ts +108 -0
- package/src/pier-task-outcome.test.mts +117 -0
- package/src/pier-task-outcome.ts +240 -0
- package/src/pier-trial-controller.test.mts +412 -0
- package/src/pier-trial-controller.ts +858 -0
- package/src/pier-trial-supervisor.mjs +352 -0
- package/src/resolve-client.ts +25 -2
- package/src/run-benchmarks-cli.mts +72 -0
- package/src/run-benchmarks-report.ts +66 -0
- package/src/run-benchmarks.test.mts +231 -0
- package/src/run-benchmarks.ts +589 -0
- package/src/smoke-structural-rollout.mts +393 -0
- package/src/swe-bench-env.test.ts +207 -0
- package/src/swe-bench-env.ts +554 -0
- package/src/swe-jail.ts +293 -0
- package/src/swe-self-improve.mts +84 -0
- package/src/swe-structural-judge-policy.test.ts +117 -0
- package/src/swe-structural-judge-policy.ts +133 -0
- package/src/swe-structural-policy.test.ts +124 -0
- package/src/swe-structural-policy.ts +132 -0
- package/src/swe-structural-provenance.test.ts +93 -0
- package/src/swe-structural-provenance.ts +138 -0
- package/src/swe-structural.mts +1260 -0
- package/src/swe-temp.ts +14 -0
- package/src/tb-container-executor.mts +234 -0
- package/src/tb-container-executor.test.mts +99 -0
- package/src/tb-supervisor-sidecar.mts +222 -0
- package/src/trata-gepa.mts +1 -1
- package/steerers/eops-itsm-population.json +1 -0
- package/tb_agents/opencode_refine_agent.py +117 -0
- package/tb_agents/opencode_router_agent.py +406 -0
- package/tb_agents/opencode_supervisor_agent.py +239 -0
- package/tb_agents/script_agent.py +66 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { execFile } from 'node:child_process'
|
|
3
|
+
import { chmod, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import test from 'node:test'
|
|
7
|
+
import { promisify } from 'node:util'
|
|
8
|
+
|
|
9
|
+
import type { AgentCandidateExecutorRequest } from '@tangle-network/agent-runtime'
|
|
10
|
+
import { InMemoryTraceStore } from '@tangle-network/agent-eval'
|
|
11
|
+
|
|
12
|
+
import { createStagedPierCandidateExecutionFixture } from './pier-agent.test-fixtures.mts'
|
|
13
|
+
import { FilePierCandidateTrialController } from './pier-trial-controller'
|
|
14
|
+
|
|
15
|
+
const execFileAsync = promisify(execFile)
|
|
16
|
+
|
|
17
|
+
function testRequest(executionId: string, executionPlanDigest: `sha256:${string}`) {
|
|
18
|
+
return {
|
|
19
|
+
staged: createStagedPierCandidateExecutionFixture(executionId),
|
|
20
|
+
context: {
|
|
21
|
+
request: {
|
|
22
|
+
executionId,
|
|
23
|
+
executionPlan: { value: { digest: executionPlanDigest } },
|
|
24
|
+
} as AgentCandidateExecutorRequest,
|
|
25
|
+
traceStore: new InMemoryTraceStore(),
|
|
26
|
+
signal: new AbortController().signal,
|
|
27
|
+
deadlineAtMs: Date.now() + 30_000,
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('an existing Pier job is rejected without deleting or starting it', async () => {
|
|
33
|
+
const root = await mkdtemp(path.join(tmpdir(), 'pier-controller-scope-'))
|
|
34
|
+
const controlRoot = path.join(root, 'control')
|
|
35
|
+
const jobsDirectory = path.join(root, 'jobs')
|
|
36
|
+
const jobName = 'already-owned'
|
|
37
|
+
const marker = path.join(jobsDirectory, jobName, 'keep')
|
|
38
|
+
const fakeDocker = path.join(root, 'docker')
|
|
39
|
+
try {
|
|
40
|
+
await mkdir(path.dirname(marker), { recursive: true })
|
|
41
|
+
await writeFile(marker, 'do not delete\n')
|
|
42
|
+
await writeFile(fakeDocker, '#!/bin/sh\nexit 0\n', { mode: 0o700 })
|
|
43
|
+
await chmod(fakeDocker, 0o700)
|
|
44
|
+
const controller = new FilePierCandidateTrialController({
|
|
45
|
+
directory: controlRoot,
|
|
46
|
+
launch: () => ({
|
|
47
|
+
command: process.execPath,
|
|
48
|
+
args: ['-e', 'process.exit(0)'],
|
|
49
|
+
cwd: root,
|
|
50
|
+
env: { ...process.env },
|
|
51
|
+
jobsDirectory,
|
|
52
|
+
jobName,
|
|
53
|
+
dockerCommand: fakeDocker,
|
|
54
|
+
readResult: async () => {
|
|
55
|
+
throw new Error('existing job must never launch')
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
})
|
|
59
|
+
const { staged, context } = testRequest(
|
|
60
|
+
'pier-job-scope',
|
|
61
|
+
`sha256:${'c'.repeat(64)}`,
|
|
62
|
+
)
|
|
63
|
+
assert.throws(
|
|
64
|
+
() => controller.start(staged, context),
|
|
65
|
+
/job name must be unique/,
|
|
66
|
+
)
|
|
67
|
+
assert.equal(await readFile(marker, 'utf8'), 'do not delete\n')
|
|
68
|
+
assert.deepEqual(await readdir(controlRoot), [])
|
|
69
|
+
} finally {
|
|
70
|
+
await rm(root, { recursive: true, force: true })
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('the Pier result wait does not add time beyond an expired deadline', async () => {
|
|
75
|
+
const root = await mkdtemp(path.join(tmpdir(), 'pier-controller-deadline-'))
|
|
76
|
+
const controlRoot = path.join(root, 'control')
|
|
77
|
+
const jobsDirectory = path.join(root, 'jobs')
|
|
78
|
+
const fakeDocker = path.join(root, 'docker')
|
|
79
|
+
const readyPath = path.join(root, 'supervisor-ready')
|
|
80
|
+
const supervisorPath = path.join(root, 'supervisor.mjs')
|
|
81
|
+
let handle: ReturnType<FilePierCandidateTrialController['start']> | undefined
|
|
82
|
+
let resultSettled: Promise<unknown> | undefined
|
|
83
|
+
try {
|
|
84
|
+
await writeFile(fakeDocker, '#!/bin/sh\nexit 0\n', { mode: 0o700 })
|
|
85
|
+
await chmod(fakeDocker, 0o700)
|
|
86
|
+
await writeFile(
|
|
87
|
+
supervisorPath,
|
|
88
|
+
`import { createReadStream } from 'node:fs'
|
|
89
|
+
import { writeFileSync } from 'node:fs'
|
|
90
|
+
import path from 'node:path'
|
|
91
|
+
const directory = process.argv[2]
|
|
92
|
+
process.on('SIGTERM', () => {
|
|
93
|
+
writeFileSync(path.join(directory, 'terminal.json'), JSON.stringify({
|
|
94
|
+
schemaVersion: 1,
|
|
95
|
+
kind: 'pier-trial-terminal',
|
|
96
|
+
status: 'stopped',
|
|
97
|
+
processExited: true,
|
|
98
|
+
containersRemoved: true,
|
|
99
|
+
}))
|
|
100
|
+
process.exit(0)
|
|
101
|
+
})
|
|
102
|
+
for await (const _chunk of createReadStream('', { fd: 3 })) {}
|
|
103
|
+
writeFileSync(${JSON.stringify(readyPath)}, 'ready')
|
|
104
|
+
setInterval(() => undefined, 1_000)
|
|
105
|
+
`,
|
|
106
|
+
)
|
|
107
|
+
const controller = new FilePierCandidateTrialController({
|
|
108
|
+
directory: controlRoot,
|
|
109
|
+
supervisorPath,
|
|
110
|
+
pollIntervalMs: 5,
|
|
111
|
+
launch: () => ({
|
|
112
|
+
command: process.execPath,
|
|
113
|
+
args: ['-e', 'process.exit(0)'],
|
|
114
|
+
cwd: root,
|
|
115
|
+
env: {},
|
|
116
|
+
jobsDirectory,
|
|
117
|
+
jobName: 'expired-deadline',
|
|
118
|
+
dockerCommand: fakeDocker,
|
|
119
|
+
readResult: async () => {
|
|
120
|
+
throw new Error('an expired trial must not read a result')
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
})
|
|
124
|
+
const { staged, context } = testRequest(
|
|
125
|
+
'pier-expired-deadline',
|
|
126
|
+
`sha256:${'e'.repeat(64)}`,
|
|
127
|
+
)
|
|
128
|
+
context.deadlineAtMs = Date.now()
|
|
129
|
+
handle = controller.start(staged, context)
|
|
130
|
+
resultSettled = handle.result.catch((error) => error)
|
|
131
|
+
|
|
132
|
+
const outcome = await Promise.race([
|
|
133
|
+
resultSettled,
|
|
134
|
+
new Promise<'pending'>((resolvePending) => setTimeout(() => resolvePending('pending'), 500)),
|
|
135
|
+
])
|
|
136
|
+
assert.notEqual(outcome, 'pending')
|
|
137
|
+
assert.match(String(outcome), /no terminal acknowledgement/)
|
|
138
|
+
} finally {
|
|
139
|
+
if (handle) await handle.terminateAndWait().catch(() => undefined)
|
|
140
|
+
if (resultSettled) await resultSettled
|
|
141
|
+
await rm(root, { recursive: true, force: true })
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('the supervisor receives only launch data and no inherited evaluator environment', async () => {
|
|
146
|
+
const root = await mkdtemp(path.join(tmpdir(), 'pier-controller-supervisor-env-'))
|
|
147
|
+
const controlRoot = path.join(root, 'control')
|
|
148
|
+
const jobsDirectory = path.join(root, 'jobs')
|
|
149
|
+
const fakeDocker = path.join(root, 'docker')
|
|
150
|
+
const supervisorPath = path.join(root, 'supervisor.mjs')
|
|
151
|
+
const previousSecret = process.env.PIER_PARENT_SECRET
|
|
152
|
+
let handle: ReturnType<FilePierCandidateTrialController['start']> | undefined
|
|
153
|
+
try {
|
|
154
|
+
process.env.PIER_PARENT_SECRET = 'must-not-be-inherited'
|
|
155
|
+
await writeFile(fakeDocker, '#!/bin/sh\nexit 0\n', { mode: 0o700 })
|
|
156
|
+
await chmod(fakeDocker, 0o700)
|
|
157
|
+
await writeFile(
|
|
158
|
+
supervisorPath,
|
|
159
|
+
`import { createReadStream, writeFileSync } from 'node:fs'
|
|
160
|
+
import path from 'node:path'
|
|
161
|
+
const chunks = []
|
|
162
|
+
for await (const chunk of createReadStream('', { fd: 3 })) chunks.push(Buffer.from(chunk))
|
|
163
|
+
const launch = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
164
|
+
const isolated = process.env.PIER_SUPERVISOR_MODE === 'explicit' &&
|
|
165
|
+
process.env.PIER_PARENT_SECRET === undefined &&
|
|
166
|
+
launch.env.PIER_PARENT_SECRET === undefined &&
|
|
167
|
+
launch.env.PIER_CHILD_SECRET === 'child-only'
|
|
168
|
+
writeFileSync(path.join(process.argv[2], 'terminal.json'), JSON.stringify({
|
|
169
|
+
schemaVersion: 1,
|
|
170
|
+
kind: 'pier-trial-terminal',
|
|
171
|
+
status: isolated ? 'completed' : 'failed',
|
|
172
|
+
processExited: true,
|
|
173
|
+
containersRemoved: true,
|
|
174
|
+
...(isolated ? {} : { error: 'supervisor inherited evaluator environment' }),
|
|
175
|
+
}))
|
|
176
|
+
`,
|
|
177
|
+
)
|
|
178
|
+
const controller = new FilePierCandidateTrialController({
|
|
179
|
+
directory: controlRoot,
|
|
180
|
+
supervisorPath,
|
|
181
|
+
pollIntervalMs: 5,
|
|
182
|
+
dockerConnection: {
|
|
183
|
+
id: 'explicit-supervisor-test',
|
|
184
|
+
env: { PIER_SUPERVISOR_MODE: 'explicit' },
|
|
185
|
+
},
|
|
186
|
+
launch: () => ({
|
|
187
|
+
command: process.execPath,
|
|
188
|
+
args: ['-e', 'process.exit(0)'],
|
|
189
|
+
cwd: root,
|
|
190
|
+
env: { PIER_CHILD_SECRET: 'child-only' },
|
|
191
|
+
jobsDirectory,
|
|
192
|
+
jobName: 'isolated-supervisor',
|
|
193
|
+
dockerCommand: fakeDocker,
|
|
194
|
+
readResult: async () => ({
|
|
195
|
+
value: { output: 'isolated' },
|
|
196
|
+
resultBytes: Uint8Array.from(Buffer.from('{}')),
|
|
197
|
+
taskPatch: new Uint8Array(),
|
|
198
|
+
}),
|
|
199
|
+
}),
|
|
200
|
+
})
|
|
201
|
+
const { staged, context } = testRequest(
|
|
202
|
+
'pier-isolated-supervisor',
|
|
203
|
+
`sha256:${'f'.repeat(64)}`,
|
|
204
|
+
)
|
|
205
|
+
handle = controller.start(staged, context)
|
|
206
|
+
assert.deepEqual((await handle.result).value, { output: 'isolated' })
|
|
207
|
+
} finally {
|
|
208
|
+
if (previousSecret === undefined) delete process.env.PIER_PARENT_SECRET
|
|
209
|
+
else process.env.PIER_PARENT_SECRET = previousSecret
|
|
210
|
+
if (handle) await handle.terminateAndWait().catch(() => undefined)
|
|
211
|
+
await rm(root, { recursive: true, force: true })
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('terminal acknowledgements reject unknown fields', async () => {
|
|
216
|
+
const root = await mkdtemp(path.join(tmpdir(), 'pier-controller-terminal-schema-'))
|
|
217
|
+
const controlRoot = path.join(root, 'control')
|
|
218
|
+
const jobsDirectory = path.join(root, 'jobs')
|
|
219
|
+
const fakeDocker = path.join(root, 'docker')
|
|
220
|
+
const supervisorPath = path.join(root, 'supervisor.mjs')
|
|
221
|
+
let handle: ReturnType<FilePierCandidateTrialController['start']> | undefined
|
|
222
|
+
try {
|
|
223
|
+
await writeFile(fakeDocker, '#!/bin/sh\nexit 0\n', { mode: 0o700 })
|
|
224
|
+
await chmod(fakeDocker, 0o700)
|
|
225
|
+
await writeFile(
|
|
226
|
+
supervisorPath,
|
|
227
|
+
`import { createReadStream, writeFileSync } from 'node:fs'
|
|
228
|
+
import path from 'node:path'
|
|
229
|
+
for await (const _chunk of createReadStream('', { fd: 3 })) {}
|
|
230
|
+
writeFileSync(path.join(process.argv[2], 'terminal.json'), JSON.stringify({
|
|
231
|
+
schemaVersion: 1,
|
|
232
|
+
kind: 'pier-trial-terminal',
|
|
233
|
+
status: 'completed',
|
|
234
|
+
processExited: true,
|
|
235
|
+
containersRemoved: true,
|
|
236
|
+
containerRemoved: true,
|
|
237
|
+
}))
|
|
238
|
+
`,
|
|
239
|
+
)
|
|
240
|
+
const controller = new FilePierCandidateTrialController({
|
|
241
|
+
directory: controlRoot,
|
|
242
|
+
supervisorPath,
|
|
243
|
+
pollIntervalMs: 5,
|
|
244
|
+
launch: () => ({
|
|
245
|
+
command: process.execPath,
|
|
246
|
+
args: ['-e', 'process.exit(0)'],
|
|
247
|
+
cwd: root,
|
|
248
|
+
env: {},
|
|
249
|
+
jobsDirectory,
|
|
250
|
+
jobName: 'strict-terminal',
|
|
251
|
+
dockerCommand: fakeDocker,
|
|
252
|
+
readResult: async () => {
|
|
253
|
+
throw new Error('a malformed terminal must not read a result')
|
|
254
|
+
},
|
|
255
|
+
}),
|
|
256
|
+
})
|
|
257
|
+
const { staged, context } = testRequest(
|
|
258
|
+
'pier-strict-terminal',
|
|
259
|
+
`sha256:${'0'.repeat(64)}`,
|
|
260
|
+
)
|
|
261
|
+
handle = controller.start(staged, context)
|
|
262
|
+
await assert.rejects(handle.result, /terminal is malformed/)
|
|
263
|
+
} finally {
|
|
264
|
+
if (handle) await handle.terminateAndWait().catch(() => undefined)
|
|
265
|
+
await rm(root, { recursive: true, force: true })
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('a fresh evaluator process terminates the persisted process and container identity', async () => {
|
|
270
|
+
const root = await mkdtemp(path.join(tmpdir(), 'pier-controller-recovery-'))
|
|
271
|
+
const controlRoot = path.join(root, 'control')
|
|
272
|
+
const jobsDirectory = path.join(root, 'jobs')
|
|
273
|
+
const jobName = 'recovery-job'
|
|
274
|
+
const trialName = 'trial-recovery'
|
|
275
|
+
const primaryContainerState = path.join(root, 'primary-container-live')
|
|
276
|
+
const otherContainerState = path.join(root, 'other-container-live')
|
|
277
|
+
const fakeDocker = path.join(root, 'docker')
|
|
278
|
+
const childPidPath = path.join(root, 'child.pid')
|
|
279
|
+
const executionId = 'pier-fresh-process-recovery'
|
|
280
|
+
const executionPlanDigest = `sha256:${'d'.repeat(64)}` as const
|
|
281
|
+
let handle: ReturnType<FilePierCandidateTrialController['start']> | undefined
|
|
282
|
+
let resultSettled: Promise<unknown> | undefined
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await writeFile(primaryContainerState, 'live\n')
|
|
286
|
+
await writeFile(otherContainerState, 'live\n')
|
|
287
|
+
await writeFile(
|
|
288
|
+
fakeDocker,
|
|
289
|
+
`#!/bin/sh
|
|
290
|
+
set -eu
|
|
291
|
+
state="\${FAKE_DOCKER_STATE:?missing FAKE_DOCKER_STATE}"
|
|
292
|
+
if test "\${1:-}" = ps; then
|
|
293
|
+
test -f "$state" && printf 'fake-container\\t${trialName}\\n'
|
|
294
|
+
exit 0
|
|
295
|
+
fi
|
|
296
|
+
if test "\${1:-}" = rm; then
|
|
297
|
+
rm -f -- "$state"
|
|
298
|
+
exit 0
|
|
299
|
+
fi
|
|
300
|
+
printf 'unexpected fake docker invocation: %s\\n' "$*" >&2
|
|
301
|
+
exit 2
|
|
302
|
+
`,
|
|
303
|
+
{ mode: 0o700 },
|
|
304
|
+
)
|
|
305
|
+
await chmod(fakeDocker, 0o700)
|
|
306
|
+
|
|
307
|
+
const controller = new FilePierCandidateTrialController({
|
|
308
|
+
directory: controlRoot,
|
|
309
|
+
dockerConnection: {
|
|
310
|
+
id: 'fake-primary',
|
|
311
|
+
env: { FAKE_DOCKER_STATE: primaryContainerState },
|
|
312
|
+
},
|
|
313
|
+
launch: () => ({
|
|
314
|
+
command: process.execPath,
|
|
315
|
+
args: [
|
|
316
|
+
'-e',
|
|
317
|
+
`const { spawn } = require('node:child_process'); const { mkdirSync, writeFileSync } = require('node:fs'); mkdirSync(${JSON.stringify(path.join(jobsDirectory, jobName, trialName))}, { recursive: true }); const child = spawn(process.execPath, ['-e', 'setInterval(() => undefined, 1_000)'], { stdio: 'ignore' }); writeFileSync(${JSON.stringify(childPidPath)}, String(child.pid)); setInterval(() => undefined, 1_000)`,
|
|
318
|
+
],
|
|
319
|
+
cwd: root,
|
|
320
|
+
env: {},
|
|
321
|
+
jobsDirectory,
|
|
322
|
+
jobName,
|
|
323
|
+
dockerCommand: fakeDocker,
|
|
324
|
+
readResult: async () => {
|
|
325
|
+
throw new Error('recovery trial must not complete normally')
|
|
326
|
+
},
|
|
327
|
+
}),
|
|
328
|
+
})
|
|
329
|
+
const { staged, context } = testRequest(executionId, executionPlanDigest)
|
|
330
|
+
handle = controller.start(staged, context)
|
|
331
|
+
resultSettled = handle.result.catch(() => undefined)
|
|
332
|
+
|
|
333
|
+
const [slot] = await readdir(controlRoot)
|
|
334
|
+
assert.ok(slot)
|
|
335
|
+
const identityPath = path.join(controlRoot, slot, 'identity.json')
|
|
336
|
+
let persisted: { state?: string; pier?: { pid: number } } = {}
|
|
337
|
+
for (let attempt = 0; attempt < 200; attempt++) {
|
|
338
|
+
persisted = JSON.parse(await readFile(identityPath, 'utf8'))
|
|
339
|
+
if (persisted.state === 'running' && persisted.pier) break
|
|
340
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 10))
|
|
341
|
+
}
|
|
342
|
+
assert.equal(persisted.state, 'running')
|
|
343
|
+
assert.ok(persisted.pier)
|
|
344
|
+
let childPid: number | undefined
|
|
345
|
+
for (let attempt = 0; attempt < 200; attempt++) {
|
|
346
|
+
try {
|
|
347
|
+
childPid = Number(await readFile(childPidPath, 'utf8'))
|
|
348
|
+
if (Number.isSafeInteger(childPid)) break
|
|
349
|
+
} catch {}
|
|
350
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 10))
|
|
351
|
+
}
|
|
352
|
+
assert.ok(childPid)
|
|
353
|
+
|
|
354
|
+
const recoveryScript = path.resolve('scripts/terminate-pier-trial.mts')
|
|
355
|
+
const recoveryArgs = [
|
|
356
|
+
'--import',
|
|
357
|
+
'tsx',
|
|
358
|
+
recoveryScript,
|
|
359
|
+
controlRoot,
|
|
360
|
+
executionId,
|
|
361
|
+
executionPlanDigest,
|
|
362
|
+
]
|
|
363
|
+
const recoveryEnvironment = {
|
|
364
|
+
...process.env,
|
|
365
|
+
PIER_DOCKER_CONNECTION_ID: 'fake-primary',
|
|
366
|
+
PIER_DOCKER_ENV_NAMES: 'FAKE_DOCKER_STATE',
|
|
367
|
+
FAKE_DOCKER_STATE: primaryContainerState,
|
|
368
|
+
}
|
|
369
|
+
await assert.rejects(
|
|
370
|
+
execFileAsync(process.execPath, recoveryArgs, {
|
|
371
|
+
cwd: path.resolve('.'),
|
|
372
|
+
env: {
|
|
373
|
+
...recoveryEnvironment,
|
|
374
|
+
PIER_DOCKER_CONNECTION_ID: 'fake-other',
|
|
375
|
+
FAKE_DOCKER_STATE: otherContainerState,
|
|
376
|
+
},
|
|
377
|
+
timeout: 20_000,
|
|
378
|
+
}),
|
|
379
|
+
(error: unknown) =>
|
|
380
|
+
String((error as { stderr?: string }).stderr).includes(
|
|
381
|
+
'requires Docker connection fake-primary',
|
|
382
|
+
),
|
|
383
|
+
)
|
|
384
|
+
const recovered = await Promise.all([
|
|
385
|
+
execFileAsync(process.execPath, recoveryArgs, {
|
|
386
|
+
cwd: path.resolve('.'),
|
|
387
|
+
env: recoveryEnvironment,
|
|
388
|
+
timeout: 20_000,
|
|
389
|
+
}),
|
|
390
|
+
execFileAsync(process.execPath, recoveryArgs, {
|
|
391
|
+
cwd: path.resolve('.'),
|
|
392
|
+
env: recoveryEnvironment,
|
|
393
|
+
timeout: 20_000,
|
|
394
|
+
}),
|
|
395
|
+
])
|
|
396
|
+
for (const worker of recovered) {
|
|
397
|
+
assert.deepEqual(JSON.parse(worker.stdout), {
|
|
398
|
+
processExited: true,
|
|
399
|
+
containersRemoved: true,
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
await assert.rejects(readFile(primaryContainerState), /ENOENT/)
|
|
403
|
+
assert.equal(await readFile(otherContainerState, 'utf8'), 'live\n')
|
|
404
|
+
assert.throws(() => process.kill(persisted.pier!.pid, 0), /ESRCH/)
|
|
405
|
+
assert.throws(() => process.kill(childPid!, 0), /ESRCH/)
|
|
406
|
+
await resultSettled
|
|
407
|
+
} finally {
|
|
408
|
+
if (handle) await handle.terminateAndWait().catch(() => undefined)
|
|
409
|
+
if (resultSettled) await resultSettled
|
|
410
|
+
await rm(root, { recursive: true, force: true })
|
|
411
|
+
}
|
|
412
|
+
})
|