CodeNomad/scripts/test-compaction-isolation-native.mjs
Pascal André a9e624baf5
fix(opencode): qualify 2.0.22 and fence parent session creation (#830)
## Summary

Qualify **OpenCode 2.0.22**, align server/UI client and plugin pins, and
recommend this release. Keep the demonstrated **2.0.7** global minimum
and **2.0.20** Codex Usage feature-local requirement.

Block the new native `parentID` create variant in the workspace proxy:
upstream ignores the supplied location and inherits the parent's
directory. Require inspectable JSON objects to close opaque-body
bypasses. Root creation and the existing authorized fork route remain
available.

Started at `dev@0983db3c` (#828 Windows test harness), then integrated
`dev@97c361c3` (#789 Code Mode presentation). Conversation remains
attached to `D:\CodeNomad`; all work and tests use explicit separate
paths and synthetic data.

## Upstream audit: 2.0.21 → 2.0.22

- Authenticated native OpenAPI retains **116 paths**, with **11 schema
differences**: session creation gains optional `parentID`,
parent-not-found response/description; provider settings gain optional
`headerTimeout` and `chunkTimeout=false`; configured model capability
overrides become partial. No new proxy route or consumed mandatory API.
- Published declarations: **4/30 client, 2/60 plugin, 3/36 protocol,
4/102 schema** files change. Plugin session domains expose `remove` and
`compact`; CodeNomad does not add calls to them. Identifier namespace
export is additive.
- An isolated native fixture proves parent creation inherits the parent
location despite a different supplied directory. Mock and
production-proxy fixtures reject owned/foreign parents; malformed values
and opaque content types are covered before any native request. Owned
root creation stays available.
- Runtime changes include HTTP header/chunk/whole-response timeout
handling and bounded timeout retries, provider cache/error/tool-history
fixes, model capability defaults, and legacy MCP shutdown cleanup.
- Lock changes are limited to six `@opencode` packages and two workspace
entries. Integrity/dependency metadata matches independently installed
published packages; clean `npm ci --ignore-scripts` passes. Optional
OpenTUI peers are not installed by CodeNomad.

## Isolated local qualification

**Windows / Node 24.20.0**, isolated profiles, databases, services and
synthetic providers/credentials. No shared-daemon or personal-history
mutations.

- Previous **2.0.21 dependencies + runtime 2.0.22**: native
pruning/history/proxy acceptance passes.
- New **2.0.22 dependencies + runtimes 2.0.7 and 2.0.22**: seven suites
pass (automation, pruning/history/navigation, per-send environment,
inclusive forks, idle/busy side questions, blank-session cleanup,
Git-degraded recovery), plus explicit native proxy suites.
- Native/UI pruning acceptance passes through the committed parent
runner, with completion and actual child exit verified. This uses #828's
early dependency-import workaround, not the old late-import order. No
claim that underlying Windows heap corruption is repaired.
- Current compaction-isolation acceptance passes: session B progresses
while session A's provider response is held; bounded outline reads
retain daemon heartbeats. Fixture runtime selection is extended to
include 2.0.22; its assertions are unchanged. This supplemental fixture
was not qualified on 2.0.7 (explicit runtime selector rejects it).
- Native Codex Usage passes on 2.0.22 with synthetic OAuth/key
credentials and mocked quota HTTP; not a live-account/WSL test.
- Native migrations **2.0.3 → 2.0.22** and **beta-19271 → 2.0.22**
preserve full history/forks/inbox/provider selection.
- Packed standalone pruning plugin installed outside the repository
passes native acceptance.
- Native npm upgrade **2.0.15 → 2.0.22** passes; authenticated daemon
remains **2.0.15 / PID 43876** before and after.
- Full server run: **794 passed / 6 skipped / 0 failed**. Final guard
revision separately passes **68 focused server tests** and native proxy
suites on minimum/current. Aggregate runs use `--test-force-exit`.
- Targeted client/store/reducer/usage UI: **63/63** with
`--conditions=browser`, `--test-force-exit`, bounded timeout. Initial
broad UI run used the wrong Node conditions and hung at
`prioritized-read.test.ts`; only its owned processes were stopped. That
incomplete run is **not green**. The same prioritized-read tests pass
under the browser condition used by CI. No production fix is inferred
from this launch correction.
- **41 browser scenarios** pass: setup/install, auth recovery, tool
images, rendered Codex Usage and compaction responsiveness. Browser
tests do not use forced exit.
- Server/UI/Electron typechecks and production server/UI/plugin builds
pass. Revalidation after #789 integration and independent gatekeeper are
reported separately below.

Local results do not certify all real providers, Linux/macOS/WSL, or
installed Electron/Tauri hosts. Historical Windows import and Git
cleanup failures remain limitations, not repaired by this upgrade.
Remote CI is not continuously monitored and no global CI-green claim is
made.

Size note: existing `packages/server/src/server/http-server.ts` is
~2,359 lines;
`packages/server/src/server/__tests__/instance-proxy.test.ts` is ~1,139
lines. Changes are narrowly scoped; no unrelated size-driven refactor.

## Independent gatekeeper

Review launched on exact head `0a7615476034eabbff54967aacc8a93ab0e6ab12`
against `dev@97c361c33f6fa7de30be5dde31c0e26e12bab574`; verdict will be
published before any admin merge. Findings will be corrected and
reviewed again until zero actionable findings.
2026-10-02 15:47:14 +02:00

383 lines
24 KiB
JavaScript

// Explicit CLI only: no discovery, shared service, user database or external provider.
import assert from "node:assert/strict"
import { execFileSync, spawn } from "node:child_process"
import { createHash } from "node:crypto"
import { createServer } from "node:http"
import { mkdtemp, mkdir, realpath, writeFile } from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { setTimeout as delay } from "node:timers/promises"
import { fileURLToPath } from "node:url"
import { OpenCode } from "@opencode/client"
import { build } from "esbuild"
const cli = process.argv[2]
if (!cli || !path.isAbsolute(cli)) throw new Error("Pass an absolute isolated CLI executable")
const contextFlag = process.argv.indexOf("--context-mib")
const contextMiB = contextFlag < 0 ? 0 : Number(process.argv[contextFlag + 1])
if (![0, 2, 8, 32].includes(contextMiB)) throw new Error("--context-mib accepts only 0, 2, 8 or 32")
const skipOutline = process.argv.includes("--skip-outline")
const temporaryRoot = path.join(os.tmpdir(), "opencode")
await mkdir(temporaryRoot, { recursive: true })
const root = await realpath(await mkdtemp(path.join(temporaryRoot, "compaction-isolation-")))
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(OPENCODE_|XDG_)/i.test(key)))
for (const key of ["XDG_DATA_HOME", "XDG_CONFIG_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"]) env[key] = path.join(root, key)
Object.assign(env, {
HOME: root, USERPROFILE: root, OPENCODE_TEST_HOME: root,
OPENCODE_CONFIG_DIR: path.join(root, "config"), OPENCODE_DB: path.join(root, "test.db"),
OPENCODE_SERVER_PASSWORD: "private-compaction-fixture", OPENCODE_CONFIG_PROJECT_DISABLE: "1",
OPENCODE_DISABLE_MODELS_FETCH: "1", OPENCODE_DISABLE_FFF: "1",
})
const runtimeVersion = execFileSync(cli, ["--version"], { cwd: root, env, encoding: "utf8" }).trim().replace(/^opencode2? v/, "")
assert.match(runtimeVersion, /^2\.0\.(19|21|22)$/, "This fixture qualifies only the requested runtimes")
await Promise.all(["config", "plugin", "pruning"].map(name => mkdir(path.join(root, name), { recursive: true })))
const bundle = await build({
entryPoints: [fileURLToPath(new URL("../packages/server/src/opencode/session-pruning/plugin.ts", import.meta.url))],
bundle: true, platform: "node", format: "esm", target: "node22", write: false,
banner: { js: 'import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);' },
})
await writeFile(path.join(root, "pruning", "index.mjs"), bundle.outputFiles[0].contents)
await writeFile(path.join(root, "plugin", "index.ts"), `
export default { id:'private-compaction-fixture', async setup(ctx) {
await ctx.session.hook('http.request', event => {
event.request.headers.set('x-fixture-kind', event.kind)
event.request.headers.set('x-fixture-session', event.sessionID)
})
await ctx.tool.transform(editor => editor.add({ name:'isolation_probe', description:'Inert fixture',
input:{type:'object',properties:{}}, options:{codemode:false}, execute:async()=>({content:'Synthetic tool result'}) }))
} }
`)
const gates = new Map(), requests = [], toolIssued = new Set()
const gate = key => {
let release
const promise = new Promise(resolve => { release = resolve })
const value = { promise, release, released: false }
gates.set(key, value)
return () => { value.released = true; release(); gates.delete(key) }
}
const summary = ["## Objective", "## Requirements", "## Decisions", "## Work State", "### Completed",
"### Active", "### Blocked", "## Next Move", "## Relevant Files", "## Important Context"]
.map(heading => `${heading}\n- Synthetic isolation fixture`).join("\n\n")
let providerError
const provider = createServer(async (request, response) => {
try {
const started = performance.now()
let raw = ""
for await (const chunk of request) raw += chunk
const bodyReceived = performance.now()
const body = JSON.parse(raw), kind = request.headers["x-fixture-kind"], sessionID = request.headers["x-fixture-session"]
const record = { kind, sessionID, started, bodyReceived, received: performance.now(), bytes: Buffer.byteLength(raw),
messageCount: body.messages?.length, markerCount: (raw.match(/NATIVE_CONTEXT_CANARY_\d+/g) ?? []).length,
payloadFillBytes: (raw.match(/Z{1024,}/g) ?? []).reduce((sum, fill) => sum + fill.length, 0) }
requests.push(record)
const pending = gates.get(`${kind}:${sessionID}`)
if (pending) await pending.promise
if (response.destroyed) return
const text = kind === "compaction" ? summary : "Synthetic conclusion"
if (!body.stream) {
response.setHeader("Content-Type", "application/json")
response.end(JSON.stringify({ id: "aux", choices: [{ message: { role: "assistant", content: text }, finish_reason: "stop" }],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 } }))
return
}
response.setHeader("Content-Type", "text/event-stream")
const chunk = (delta, finish_reason = null) => response.write(`data: ${JSON.stringify({
id: "fixture", object: "chat.completion.chunk", model: "fixture",
choices: [{ index: 0, delta, finish_reason }],
...(finish_reason ? { usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 } } : {}),
})}\n\n`)
chunk({ role: "assistant" })
const tool = kind === "primary" && !toolIssued.has(sessionID)
if (tool) {
toolIssued.add(sessionID)
chunk({ tool_calls: [{ index: 0, id: `inert-${sessionID}`, type: "function", function: { name: "isolation_probe", arguments: "{}" } }] })
} else {
// Exercise real compaction delta events, without an artificial slow observer.
for (let i = 0; i < text.length; i += 16) chunk({ content: text.slice(i, i + 16) })
}
chunk({}, tool ? "tool_calls" : "stop")
response.end("data: [DONE]\n\n")
record.responded = performance.now()
} catch (error) { providerError = error; response.destroy(error) }
})
await new Promise(resolve => provider.listen(0, "127.0.0.1", resolve))
env.OPENCODE_CONFIG_CONTENT = JSON.stringify({
model: "fixture/fixture",
// Fixture-only: preserve the entire imported corpus in a manual summary request.
...(contextMiB ? { compaction: { auto: false, keep: { tokens: 0 } } } : {}),
providers: { fixture: { package: "@opencode/ai/providers/openai-compatible", settings: {
baseURL: `http://127.0.0.1:${provider.address().port}/v1`, apiKey: "fixture-only",
}, models: { fixture: contextMiB ? { limit: { context: 16777216, output: 8192 } } : {} } } },
plugins: [path.join(root, "plugin"), { package: path.join(root, "pruning"), options: { databasePath: env.OPENCODE_DB } }],
})
let output = "", observerError, spawnError
const child = spawn(cli, ["serve", "--hostname", "127.0.0.1", "--port", "0", "--log-level", "debug", "--print-logs"], { cwd: root, env, windowsHide: true })
const stopped = new Promise(resolve => child.once("close", resolve))
child.on("error", error => { spawnError = error })
child.stdout.on("data", data => { output += data })
child.stderr.on("data", data => { output += data })
const streams = new AbortController(), events = [], measurements = []
let phase = "setup"
const report = { runtimeVersion, root, pid: child.pid, contextMiB, skipOutline, measurements, requests, outline: undefined,
pruningBundleSha256: createHash("sha256").update(bundle.outputFiles[0].contents).digest("hex") }
let observer, client, releaseA, releaseB
const options = () => ({ signal: AbortSignal.timeout(15_000) })
async function until(predicate, timeout = 30_000) {
const end = performance.now() + timeout
while (performance.now() < end) {
if (providerError) throw providerError
if (observerError) throw observerError
if (spawnError) throw spawnError
if (child.exitCode !== null) throw new Error("Private server exited early")
if (await predicate()) return
await delay(20)
}
throw new Error("Private fixture condition timed out")
}
async function measured(name, call) {
const started = performance.now()
const startedPhase = phase
try { const value = await call(); const finished = performance.now(); measurements.push({ name, started, finished, startedPhase, finishedPhase: phase, ms: finished - started, status: "ok" }); return value }
catch (error) { const finished = performance.now(); measurements.push({ name, started, finished, startedPhase, finishedPhase: phase, ms: finished - started, status: "failed" }); throw error }
}
const wait = sessionID => client.session.wait({ sessionID }, options())
function distribution(values) {
const sorted = [...values].sort((a, b) => a - b)
return { count: sorted.length, min: sorted[0], median: sorted[Math.floor(sorted.length / 2)], max: sorted.at(-1) }
}
try {
await until(() => /http:\/\/127\.0\.0\.1:\d+/.test(output))
const baseUrl = output.match(/http:\/\/127\.0\.0\.1:\d+/)[0]
report.baseUrl = baseUrl
client = OpenCode.make({ baseUrl, headers: { Authorization: `Basic ${Buffer.from("opencode:private-compaction-fixture").toString("base64")}` } })
const info = await client.server.info(options())
assert.equal(info.version, runtimeVersion)
assert.equal(info.pid, child.pid, "The endpoint must belong to the child we created")
observer = (async () => {
for await (const event of client.event.subscribe({ signal: streams.signal })) {
events.push({ phase, type: event.type, sessionID: event.data?.sessionID, created: event.created, received: Date.now() })
}
})().catch(error => { if (!streams.signal.aborted) observerError = error })
await until(() => events.some(event => event.type === "server.connected"))
const location = { directory: root }
let a = await client.session.create({ location, title: "Private compaction A" }, options())
const b = await client.session.create({ location, title: "Private witness B" }, options())
report.sessions = { a: a.id, b: b.id }
await until(async () => {
const plugins = (await client.plugin.list({ location }, options())).data
const failed = plugins.filter(item => item.source.type !== "builtin" && item.state.status === "failed")
assert.equal(failed.length, 0, JSON.stringify(failed))
return plugins.some(item => item.id === "codenomad-session-pruning" && item.state.status === "active")
})
phase = "seed"
for (const session of [a, b]) {
await client.session.prompt({ sessionID: session.id, text: "Use isolation_probe, then conclude." }, options())
await wait(session.id)
}
const nativeMessages = (await client.message.list({ sessionID: a.id, limit: 20 }, options())).data
const template = nativeMessages.find(message => message.type === "assistant" && message.content.some(part => part.type === "tool"))
assert(template, "A real native assistant/tool template must exist")
const userTemplate = nativeMessages.find(message => message.type === "user")
assert(userTemplate)
const tool = template.content.find(part => part.type === "tool")
assert.equal(tool.state.status, "completed")
if (contextMiB) {
const exportedA = await client.session.export({ sessionID: a.id }, options())
const count = contextMiB * 4, bytes = 262144
let fillBytes = 0
const imported = Array.from({ length: count }, (_, index) => {
const marker = `NATIVE_CONTEXT_CANARY_${String(index).padStart(4, "0")}\n`
const text = marker + "Z".repeat(bytes - marker.length)
fillBytes += bytes - marker.length
return [
{ ...structuredClone(userTemplate), id: `msg_context_user_${String(index).padStart(4, "0")}`, text: `Inspect synthetic item ${index}.` },
{ ...structuredClone(template), id: `msg_context_assistant_${String(index).padStart(4, "0")}`,
content: [{ ...structuredClone(tool), id: `tool_context_${index}`, state: { ...structuredClone(tool.state), content: [{ type: "text", text }] } }] },
]
}).flat()
// Native manual compaction retains the current turn even with keep.tokens=0.
// A small trailing turn puts every large tool result in the summarized prefix.
imported.push(
{ ...structuredClone(userTemplate), id: "msg_context_tail_user", text: "Corpus complete; retain this small trailing turn." },
{ ...structuredClone(template), id: "msg_context_tail_assistant", content: [{ type: "text", text: "Synthetic trailing conclusion" }] },
)
await client.session.remove({ sessionID: a.id }, options())
a = await client.session.import({ ...exportedA, messages: imported, location }, { signal: AbortSignal.timeout(60_000) })
report.sessions.a = a.id
const stored = await client.session.context({ sessionID: a.id }, { signal: AbortSignal.timeout(60_000) })
const storedTools = stored.filter(message => message.type === "assistant").flatMap(message => message.content.filter(part => part.type === "tool"))
const storedBytes = storedTools.reduce((sum, part) => sum + part.state.content.reduce((total, content) => total + Buffer.byteLength(content.text ?? ""), 0), 0)
assert.equal(storedTools.length, count)
assert.equal(storedBytes, contextMiB * 1048576)
assert.equal(stored.length, imported.length)
report.contextCorpus = { turns: count + 1, payloadTurns: count, messages: imported.length, payloadBytes: storedBytes, payloadFillBytes: fillBytes,
keepTokens: 0, modelContextLimit: 16777216 }
}
// Seeding is outside the timed compaction/outline intervals and uses native import.
let corpus
if (!skipOutline) {
const empty = await client.session.create({ location, title: "Private outline corpus" }, options())
const exported = await client.session.export({ sessionID: empty.id }, options())
await client.session.remove({ sessionID: empty.id }, options())
const payload = "x".repeat(262144)
const messages = Array.from({ length: 128 }, (_, index) => ({ ...structuredClone(template), id: `msg_isolation_${index}`,
content: [{ ...structuredClone(tool), id: `tool_isolation_${index}`, state: { ...structuredClone(tool.state), content: [{ type: "text", text: payload }] } }],
}))
corpus = await client.session.import({ ...exported, messages, location }, { signal: AbortSignal.timeout(60_000) })
}
// Flush seed observations before the measured phase; import is not compaction.
await delay(100)
phase = "assembly-before-provider"
const compactStart = performance.now()
releaseA = gate(`compaction:${a.id}`)
const admission = measured("A.compact.admission", () => client.session.compact({ sessionID: a.id }, options()))
admission.catch(() => {})
let probingAssembly = true
const assemblyProbes = (async () => {
while (probingAssembly && !requests.some(item => item.kind === "compaction" && item.sessionID === a.id)) {
await Promise.all([
measured("assembly.info", () => client.server.info(options())),
measured("assembly.B.get", () => client.session.get({ sessionID: b.id }, options())),
])
if (probingAssembly) await delay(5)
}
})()
assemblyProbes.catch(() => {})
const earlyB = (async () => {
await measured("assembly.B.prompt.admission", () => client.session.prompt({ sessionID: b.id, text: "Conclude during A summary assembly." }, options()))
await measured("assembly.B.wait", () => wait(b.id))
assert.equal((await client.session.get({ sessionID: b.id }, options())).outcome, "succeeded")
})()
earlyB.catch(() => {})
const admitted = await admission
assert.equal(typeof admitted.time.created, "number")
try { await until(() => requests.some(item => item.kind === "compaction" && item.sessionID === a.id)) }
finally { probingAssembly = false; await assemblyProbes }
const assembled = requests.find(item => item.kind === "compaction" && item.sessionID === a.id)
const admittedAt = measurements.find(item => item.name === "A.compact.admission").finished
report.compactionAssembly = { submissionToProviderHeadersMs: assembled.started - compactStart,
admissionResponseToProviderHeadersMs: assembled.started - admittedAt,
submissionToProviderBodyMs: assembled.bodyReceived - compactStart, providerParseMs: assembled.received - assembled.bodyReceived,
providerBytes: assembled.bytes, providerMessages: assembled.messageCount, markerCount: assembled.markerCount, payloadFillBytes: assembled.payloadFillBytes }
if (contextMiB) {
assert.equal(assembled.markerCount, report.contextCorpus.payloadTurns, "Provider must receive every corpus tool marker")
assert.equal(assembled.payloadFillBytes, report.contextCorpus.payloadFillBytes, "Stored corpus bytes must really reach the compaction provider")
}
await until(() => events.some(event => event.sessionID === a.id && event.type === "session.compaction.started"))
await earlyB
assert(!events.some(event => event.sessionID === a.id && event.type === "session.compaction.ended"))
report.earlyBCompletedBeforeReleaseA = true
report.assemblyWitnesses = measurements.filter(item => item.name.startsWith("assembly.")).map(item => ({ ...item,
startedBeforeProviderHeaders: item.started < assembled.started, completedBeforeProviderHeaders: item.finished < assembled.started,
completedBeforeProviderBody: item.finished < assembled.bodyReceived }))
for (const name of ["assembly.info", "assembly.B.get", "assembly.B.prompt.admission"]) {
assert(report.assemblyWitnesses.some(item => item.name === name && item.startedBeforeProviderHeaders), `${name} must be issued before A reaches the provider`)
}
report.preProviderLatencyMs = Object.fromEntries([...new Set(report.assemblyWitnesses.map(item => item.name))]
.map(name => [name, distribution(report.assemblyWitnesses.filter(item => item.name === name && item.startedBeforeProviderHeaders).map(item => item.ms))]))
report.whollyPreProviderLatencyMs = Object.fromEntries([...new Set(report.assemblyWitnesses.map(item => item.name))]
.map(name => [name, distribution(report.assemblyWitnesses.filter(item => item.name === name && item.completedBeforeProviderHeaders).map(item => item.ms))]))
report.bCompletedBeforeProviderHeaders = report.assemblyWitnesses.some(item => item.name === "assembly.B.wait" && item.completedBeforeProviderHeaders)
phase = "held-compaction"
let aFinished = false
const waitingA = wait(a.id).then(() => { aFinished = true })
// Attach a rejection handler immediately; still await the actual result later.
waitingA.catch(() => {})
for (let sample = 0; sample < 4; sample++) {
await Promise.all([
measured("held.info", () => client.server.info(options())),
measured("held.A.get", () => client.session.get({ sessionID: a.id }, options())),
measured("held.B.get", () => client.session.get({ sessionID: b.id }, options())),
measured("held.active", () => client.session.active(options())),
])
await delay(50)
}
phase = "both-held"
releaseB = gate(`primary:${b.id}`)
const beforeB = requests.length
await measured("held.B.prompt.admission", () => client.session.prompt({ sessionID: b.id, text: "Conclude while A summary is held." }, options()))
await until(() => requests.slice(beforeB).some(item => item.kind === "primary" && item.sessionID === b.id))
report.activeWhileBothHeld = await measured("both-held.active", () => client.session.active(options()))
assert(report.activeWhileBothHeld[a.id] && report.activeWhileBothHeld[b.id], "Both native executions must be active")
await measured("both-held.B.get", () => client.session.get({ sessionID: b.id }, options()))
releaseB(); releaseB = undefined
await measured("A-held.B.wait", () => wait(b.id))
const completedB = await client.session.get({ sessionID: b.id }, options())
assert.equal(completedB.outcome, "succeeded", "B must succeed, not simply stop after an error")
const messagesB = (await client.message.list({ sessionID: b.id, limit: 8 }, options())).data
assert(messagesB.some(message => message.type === "assistant" && message.content.some(part => part.type === "text" && part.text === "Synthetic conclusion")))
assert.equal(aFinished, false, "B completed independently before releasing A")
assert(!events.some(event => event.sessionID === a.id && event.type === "session.compaction.ended"))
report.bCompletedBeforeReleaseA = true
if (!skipOutline) {
phase = "outline-under-held-compaction"
let outlining = true
const outlineStart = performance.now(), heartbeats = []
const heartbeat = (async () => {
while (outlining) {
const start = performance.now()
await client.server.info(options())
heartbeats.push({ startedMs: start - outlineStart, ms: performance.now() - start })
if (outlining) await delay(5)
}
})()
heartbeat.catch(() => {})
try {
const outline = (await measured("A-held.outline", () => client.rpc.call({ rpcID: "codenomad.session-pruning", method: "outline", location,
input: { sessionID: corpus.id } }, options()))).output
assert.equal(outline.status, "outline", JSON.stringify(outline))
assert.equal(outline.entries.length, 128)
assert(outline.entries.every(entry => entry.tools === 1 && entry.toolName === tool.name))
const elapsedMs = performance.now() - outlineStart
report.outline = { count: 128, payloadBytes: 262144, elapsedMs, heartbeats,
completedHeartbeatsDuringOutline: heartbeats.filter(item => item.startedMs + item.ms <= elapsedMs).length }
} finally { outlining = false; await heartbeat }
}
assert.equal(aFinished, false)
phase = "held-compaction"
// A genuine pending provider wait, not merely a race through a fast reply.
await delay(1_000)
await measured("held-final.B.get", () => client.session.get({ sessionID: b.id }, options()))
await measured("held-final.info", () => client.server.info(options()))
assert.equal(aFinished, false)
report.aHeldMs = performance.now() - compactStart
const releasedAt = performance.now()
phase = "summary-release"
releaseA(); releaseA = undefined
await waitingA
report.aCompletionAfterReleaseMs = performance.now() - releasedAt
const context = await client.session.context({ sessionID: a.id }, options())
assert(context.some(message => message.type === "compaction" && message.status === "completed"), "Real native compaction must finish successfully")
await until(() => events.some(event => event.sessionID === a.id && event.type === "session.compaction.ended"))
assert(!events.some(event => event.sessionID === a.id && event.type === "session.compaction.failed"))
report.eventCounts = Object.fromEntries([...new Set(events.map(event => event.type))].map(type => [type, events.filter(event => event.type === type).length]))
const business = events.filter(event => event.type.startsWith("session.") && typeof event.created === "number")
report.eventDeliveryLagByPhaseMs = Object.fromEntries([...new Set(business.map(event => event.phase))]
.map(name => [name, distribution(business.filter(event => event.phase === name).map(event => event.received - event.created))]))
report.events = events
report.latencyByOperationMs = Object.fromEntries([...new Set(measurements.map(item => item.name))]
.map(name => [name, distribution(measurements.filter(item => item.name === name).map(item => item.ms))]))
report.status = "passed"
console.log(JSON.stringify({ ...report, events: undefined, measurements: undefined, requests: undefined, assemblyWitnesses: undefined }, null, 2))
console.log(`PASS: B completed before releasing A's provider; corpus=${contextMiB} MiB, outline=${!skipOutline}`)
} catch (error) {
report.status = "failed"
report.error = String(error.stack ?? error)
throw error
} finally {
releaseA?.(); releaseB?.()
for (const value of gates.values()) value.release()
streams.abort()
await observer
child.kill()
await stopped
provider.closeAllConnections()
await new Promise(resolve => provider.close(resolve))
await writeFile(path.join(root, "server.log"), output)
await writeFile(path.join(root, "result.json"), JSON.stringify(report, null, 2))
console.log(`Private fixture retained at ${root}; only its child was stopped`)
}