mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-10-24 23:31:20 +00:00
## Summary This PR removes OpenCode compatibility code that nothing can reach any more: - Code that was already dead at the former 2.0.27 minimum, and is still dead at the current 2.1.0 minimum (#1029). - Handlers for events and data shapes that OpenCode V2 never produces. Each item was checked against the upstream 2.0.27 and 2.1.0 sources before removal. Kept on purpose: - Workarounds for upstream bugs or limits that are still present in 2.1.0. - Code that reads conversations written by older runtimes: V1-migrated tool names and metadata, `workspaceID` data, task summary titles, `session-legacy` replay. - Code that recognises and upgrades old CLIs and live older daemons: 1.x replacement, beta recognition, 2.0.11 service discovery, 2.0.15 native upgrade, `/api/status` and `/api/health` identification. ## Removed or changed **Server** - Negotiation: one "not modern → `canonical_api`" rule replaces the pre-2.0.4 legacy schema recognition, and runs before #1029's `native_contract` check. - The transport no longer rewrites `/api/status`. - Desktop skills no longer send the pre-2.0.4 `location` and `slash` fields. - The legacy `auth.json` usage fallback for daemons without `credential.list` (pre-2.0.20) is removed, along with its UI reason and the i18n key in 10 locales. A missing endpoint still fails safely. - Workspace selectors are rejected up front, without a native resolution request. - CLI service probes try `/api/info` first. An admitted daemon no longer costs two authenticated 404s on every check. **UI** - Remaining V1 `session.idle`/`session.status` branches: Missions store, pending discovery (UI and server), `eventAffectsMessages`. - `server.instance.disposed` and the now-unused `rehydrateInstance`. - The V1 `worktree.ready` envelope (the handler now reads the V2 shape). - V1 tool-state titles and the `success` status alias. - Part `time.start` and `time.end`. - Text stored as objects or segment arrays. - `deriveMessageStatus`. - The V1 `error.data.message` shape. - `projectID` and `location` fallbacks on `session.created`. - The replay-only `session.message.content.updated`. - The unreachable `output.answers` fallback. **Fixtures, Missions and docs** - Legacy branches removed from the location, fork, families, pruning and automation native fixtures. The WSL setup fixture now compares against the reported version instead of 2.0.11. - Unused 2.0.21 launch helpers removed. - The Missions native and trajectory fixtures also run on the minimum lane. - The capability proof checks the reported version. - Stale 2.0.2x labels now say "as of the 2.1.0 minimum". **Follow-ups from the #1029 post-merge review** - Test fixtures no longer send the ignored `recent: ""` compaction field (18 sites). - The manual compaction-isolation fixture accepts any published version at or above the minimum. - `CONTRIBUTING.md` wording updated. 114 files, +319 / −1117: source +90 / −487, tests/fixtures/CI +214 / −604, i18n −10, docs +15 / −16. ## Validation (rebased on dev with #1029) **Typechecks:** server and UI pass. **Server tests** - Touched areas: 480/480. - Full server CI step: 2430 pass, 0 fail, 7 skipped, 1 cancelled. The cancelled test is the `index.test.ts` real-process shutdown test; it hit its 5 s timeout under load. It passes alone (4/4), and that file is unchanged versus dev. **UI unit tests** - Touched files: 230/230. - CI steps: 574/574, 200/200 and 519/519. **Browser suites**, one at a time: - Provider and service usage: provider-usage 13/13, service-usage 3/3. - Missions: mission-visibility 3/3, mission-control-feedback 5/5, mission-lifecycle-revision 10/10, mission-lifecycle-navigation 10/10, mission-current-passage 1/1, mission-native-family 11/11. - Transcript and rendering: compaction-responsiveness 5/5, session-rendering 47/47, reasoning-collapse 3/3, render-cost 17/17, tool-images 4/4, system-messages 10/10. - Other: search-highlight 11 pass (1 skipped by its env gate), panel-extensions 13/13. **Native fixtures on an isolated 2.1.0 CLI:** location, session-fork, automation, session-pruning, missions native, missions trajectory, provider-usage, Missions capability proof and compaction-isolation all pass. The WSL setup fixture was not run (it needs a WSL distro with a Linux CLI). ## Left for later - `RuntimeIdentity.discovery` is now write-only. - `ownsLocation` has an unused `_client` parameter (about 30 callers). - A test-only V1 translation in `auto-accept-manager.test.ts`. - Out of scope: HTML-entity decoding (a product decision), the `MutableHashMap` loop, the pruning-UI import-order workaround, unused log-based Missions readers, and the unshipped managed-owner module with its 2.0.24-pinned test. Oversized source files touched: `stores/instances.ts` ~2,209, `message-block.tsx` ~1,601, `workspaces/manager.ts` ~1,104, `tool-call.css` ~1,047, `tool-call.tsx` ~865, `opencode-data.ts` ~865, `api-types.ts` ~784, `session-events.ts` ~606, `recurring-day-fixture.ts` ~535.
279 lines
17 KiB
JavaScript
279 lines
17 KiB
JavaScript
// Real Windows -> WSL official lifecycle, authenticated native connection and
|
|
// plugin provisioning. The wrapper forces every command into fresh fixture state.
|
|
import assert from "node:assert/strict"
|
|
import { execFile } from "node:child_process"
|
|
import { randomUUID } from "node:crypto"
|
|
import { createServer } from "node:net"
|
|
import { watch } from "node:fs"
|
|
import { mkdtemp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"
|
|
import os from "node:os"
|
|
import path from "node:path"
|
|
import { setTimeout as delay } from "node:timers/promises"
|
|
import { tsImport } from "tsx/esm/api"
|
|
import { OpenCode } from "@opencode/client"
|
|
import { Service } from "@opencode/client/service"
|
|
import { boundedFixtureOperation } from "./fixtures/wsl-fixture-bounds.mjs"
|
|
|
|
const deadlineAt = Date.now() + 180_000
|
|
const signal = AbortSignal.timeout(180_000)
|
|
|
|
const [distro, cli, mode = "aliased", check = ""] = process.argv.slice(2)
|
|
if (process.platform !== "win32" || !distro || !cli?.startsWith("/")) throw new Error("Pass a WSL distro and absolute Linux CLI path on Windows")
|
|
assert.ok(["native", "mounted", "aliased"].includes(mode), "Config mode must be native, mounted or aliased")
|
|
assert.ok(check === "" || check === "reload-safety", "Optional check must be reload-safety")
|
|
const quote = value => `'${value.replaceAll("'", `'"'"'`)}'`
|
|
function wsl(script, cleanup = false) {
|
|
if (!cleanup) signal.throwIfAborted()
|
|
return new Promise((resolve, reject) => {
|
|
const child = execFile("wsl.exe", ["--distribution", distro, "--exec", "/bin/bash", "-s"],
|
|
{ encoding: "utf8", timeout: cleanup ? 15_000 : Math.max(1, Math.min(60_000, deadlineAt - Date.now())),
|
|
...(!cleanup ? { signal } : {}), maxBuffer: 1024 * 1024, windowsHide: true },
|
|
(error, stdout, stderr) => error ? reject(new Error(`${error.message}\n${stderr}`)) : resolve(stdout.trim()))
|
|
child.stdin.end(`set -euo pipefail\n${script}\n`)
|
|
})
|
|
}
|
|
const parent = path.join(os.tmpdir(), "opencode")
|
|
await mkdir(parent, { recursive: true })
|
|
const mounted = await mkdtemp(path.join(parent, "codenomad-wsl-setup-"))
|
|
const mount = await wsl(`wslpath -au ${quote(mounted)}`)
|
|
const root = await wsl('mkdir -p "$HOME/.cache"\nmktemp -d "$HOME/.cache/codenomad-wsl-setup-XXXXXX"')
|
|
const password = randomUUID(), wrapper = `${root}/opencode-fixture`
|
|
const config = mode === "native" ? `${root}/config` : mode === "mounted" ? `${mount}/config` : `${root}/config-alias`
|
|
const unc = directory => `\\\\wsl.localhost\\${distro}${directory.replaceAll("/", "\\")}`
|
|
const configFile = mode === "native" ? unc(`${config}/opencode.json`) : path.join(mounted, "config/opencode.json")
|
|
// Stable CLI channels default to 49374 even with different XDG roots. Allocate
|
|
// an unused Windows port and verify it is unused in Linux before configuring it.
|
|
const reservation = createServer()
|
|
await new Promise((resolve, reject) => { reservation.once("error", reject); reservation.listen(0, "127.0.0.1", resolve) })
|
|
const port = reservation.address().port
|
|
await new Promise(resolve => reservation.close(resolve))
|
|
await wsl(`mkdir -p ${quote(`${mount}/config`)} ${quote(`${root}/config`)} ${quote(`${root}/home`)} ${quote(`${root}/repo`)}
|
|
${mode === "aliased" ? `ln -s ${quote(`${mount}/config`)} ${quote(config)}` : ""}
|
|
mkdir -m 700 ${quote(`${root}/runtime`)}
|
|
python3 -c ${quote(`import socket; s=socket.socket(); s.bind(('127.0.0.1', ${port})); s.close()`)}`)
|
|
const environment = {
|
|
PATH: "/usr/local/bin:/usr/bin:/bin", HOME: `${root}/home`, USERPROFILE: `${root}/home`,
|
|
XDG_DATA_HOME: `${root}/data`, XDG_CONFIG_HOME: `${root}/xdg-config`, XDG_STATE_HOME: `${root}/state`,
|
|
XDG_CACHE_HOME: `${root}/cache`, XDG_RUNTIME_DIR: `${root}/runtime`, OPENCODE_TEST_HOME: `${root}/home`, OPENCODE_CONFIG_DIR: config,
|
|
OPENCODE_DB: `${root}/fixture.db`,
|
|
OPENCODE_CONFIG_CONTENT: JSON.stringify({ plugins: [`${root}/probe-plugin`] }),
|
|
OPENCODE_CONFIG_PROJECT_DISABLE: "1", OPENCODE_DISABLE_MODELS_FETCH: "1", OPENCODE_DISABLE_FFF: "1",
|
|
}
|
|
let reloadProvider, reloadSafety, lifecycle, shared, automation, pruning, configWatcher
|
|
let started = false
|
|
const evidence = { root, mounted, mode, port }
|
|
async function until(predicate, message) {
|
|
const deadline = Math.min(deadlineAt, Date.now() + 30_000)
|
|
while (true) {
|
|
const options = { signal: AbortSignal.any([signal, AbortSignal.timeout(Math.max(1, deadline - Date.now()))]) }
|
|
if (await boundedFixtureOperation(() => predicate(options), deadline, message, signal)) return
|
|
await delay(100, undefined, { signal })
|
|
}
|
|
}
|
|
async function startOrRestart(restart = false) {
|
|
signal.throwIfAborted()
|
|
const startDeadline = Math.min(deadlineAt, Date.now() + 30_000)
|
|
try { return await (restart ? lifecycle.restart(startDeadline) : lifecycle.ensure(startDeadline)) }
|
|
catch (error) {
|
|
if (!error.message.startsWith("Cannot reach the WSL OpenCode service from Windows")) throw error
|
|
// Forwarding may trail Linux readiness. Only rediscover our guarded service.
|
|
let endpoint
|
|
await until(async () => {
|
|
try { endpoint = await lifecycle.discover(Math.min(deadlineAt, Date.now() + 15_000)); return Boolean(endpoint) }
|
|
catch (retry) {
|
|
if (!retry.message.startsWith("Cannot reach the WSL OpenCode service from Windows")) throw retry
|
|
return false
|
|
}
|
|
}, "Fixture WSL localhost forwarding did not become ready")
|
|
return endpoint
|
|
}
|
|
}
|
|
async function runFixture() {
|
|
if (check === "reload-safety") {
|
|
const { startReloadSafetyProvider } = await import("./fixtures/wsl-reload-safety.mjs")
|
|
reloadProvider = await startReloadSafetyProvider({ distro, root, environment, unc, signal })
|
|
environment.OPENCODE_CONFIG_CONTENT = JSON.stringify({
|
|
plugins: [`${root}/probe-plugin`], model: "fixture/fixture",
|
|
providers: { fixture: { package: "@opencode/ai/providers/openai-compatible",
|
|
settings: { baseURL: `http://127.0.0.1:${reloadProvider.port}/v1`, apiKey: "fixture" }, models: { fixture: {} } } },
|
|
})
|
|
}
|
|
// Check the isolated registration and Linux process environment before any CLI
|
|
// command can probe or stop it. Never infer ownership from an endpoint or PID.
|
|
const guard = `import json, os, pathlib, socket
|
|
root = pathlib.Path(${JSON.stringify(root)})
|
|
registration = root / 'state/opencode/service.json'
|
|
if registration.exists():
|
|
info = json.loads(registration.read_text())
|
|
assert info['url'] == 'http://127.0.0.1:${port}', 'Unexpected fixture endpoint'
|
|
env = pathlib.Path('/proc/%s/environ' % info['pid']).read_bytes().split(b'\\0')
|
|
for key, value in json.loads(${JSON.stringify(JSON.stringify(environment))}).items():
|
|
assert (key + '=' + value).encode() in env, 'Fixture process ownership mismatch: ' + key
|
|
else:
|
|
s = socket.socket()
|
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
s.bind(('127.0.0.1', ${port}))
|
|
s.close()
|
|
`
|
|
const script = `#!/bin/sh\nset -eu\n/usr/bin/python3 ${quote(`${root}/guard.py`)}\ncd ${quote(`${root}/repo`)}\nexec env -i ${Object.entries(environment).map(([key, value]) => `${key}=${quote(value)}`).join(" ")} ${quote(cli)} "$@"\n`
|
|
await wsl(`cat > ${quote(wrapper)} <<'CODENOMAD_FIXTURE'
|
|
${script}CODENOMAD_FIXTURE
|
|
cat > ${quote(`${root}/guard.py`)} <<'CODENOMAD_GUARD'
|
|
${guard}CODENOMAD_GUARD
|
|
chmod 700 ${quote(wrapper)}`)
|
|
await wsl(`mkdir ${quote(`${root}/probe-plugin`)}
|
|
cat > ${quote(`${root}/probe-plugin/index.ts`)} <<'CODENOMAD_PLUGIN'
|
|
import { appendFile } from 'node:fs/promises'
|
|
export default { id: 'wsl-fixture-probe', async setup(ctx) {
|
|
await appendFile(${JSON.stringify(`${root}/plugin-setup.log`)}, 'setup\\n')
|
|
await ctx.session.hook('http.request', event => event.request.headers.set('x-reload-kind', event.kind))
|
|
} }
|
|
CODENOMAD_PLUGIN`)
|
|
await wsl(`${quote(wrapper)} service set port ${port}
|
|
${quote(wrapper)} service set password ${quote(password)}`)
|
|
await writeFile(configFile, "{}\n")
|
|
const { WslOpenCodeService } = await tsImport("../packages/server/src/workspaces/wsl-opencode-service.ts", import.meta.url)
|
|
const { OpenCodeSharedService } = await tsImport("../packages/server/src/workspaces/opencode-service.ts", import.meta.url)
|
|
const { resolveDesktopPluginPaths } = await tsImport("../packages/server/src/opencode/desktop-plugin-paths.ts", import.meta.url)
|
|
const { DesktopPluginLifecycle, prepareDesktopPluginPresence } = await tsImport("../packages/server/src/opencode/desktop-plugin-lifecycle.ts", import.meta.url)
|
|
lifecycle = new WslOpenCodeService({ distro, binary: wrapper, timeoutMs: 30_000 })
|
|
shared = new OpenCodeSharedService({ headers: Service.headers, makeClient: options => OpenCode.make({ ...options,
|
|
fetch: (input, init) => options.fetch(input, { ...init,
|
|
signal: AbortSignal.any([signal, AbortSignal.timeout(15_000), ...(init?.signal ? [init.signal] : [])]),
|
|
}),
|
|
}) })
|
|
automation = new DesktopPluginLifecycle("automation")
|
|
pruning = new DesktopPluginLifecycle("session-pruning")
|
|
assert.equal(await lifecycle.discover(Math.min(deadlineAt, Date.now() + 15_000)), undefined, "Fresh fixture must never discover an existing service")
|
|
started = true
|
|
const endpoint = await startOrRestart()
|
|
assert.equal(endpoint.url, `http://127.0.0.1:${port}`)
|
|
const options = { kind: "lifecycle", identity: `wsl-fixture:${root}`, lifecycle }
|
|
const client = await shared.client(options, { deadlineAt })
|
|
const before = await client.server.info()
|
|
evidence.started = { endpoint: endpoint.url, pid: before.pid, version: before.version }
|
|
console.log(`Authenticated isolated ${distro} service: ${endpoint.url}, PID ${before.pid}; ${root}`)
|
|
// The selected isolated CLI, not a fixed release label, names the expected daemon.
|
|
assert.equal(before.version, (await wsl(`${quote(wrapper)} --version`)).split(/\s+/).at(-1).replace(/^v/, ""))
|
|
const connection = await shared.acquire()
|
|
const assertCurrent = () => { signal.throwIfAborted(); connection.assertCurrent() }
|
|
const paths = await resolveDesktopPluginPaths({ ...connection, assertCurrent }, { kind: "wsl", distro, binary: wrapper }, Math.min(deadlineAt, Date.now() + 15_000))
|
|
evidence.paths = paths
|
|
if (mode === "native") assert.ok(paths.config.startsWith(`\\\\wsl.localhost\\${distro}\\`))
|
|
else assert.equal(paths.config.toLowerCase(), path.dirname(configFile).toLowerCase(), "Mounted discovery roots, including symlink aliases, need Windows-native access paths")
|
|
assert.equal(await readFile(path.join(paths.config, "opencode.json"), "utf8"), "{}\n")
|
|
assert.ok(!paths.data.startsWith(`${paths.config}\\`), "Presence leases must stay outside the aliased/mounted config tree")
|
|
const location = { directory: `${root}/repo` }
|
|
await client.location.get({ location })
|
|
await until(async request => (await client.plugin.list({ location }, request)).data.some(plugin => plugin.id === "wsl-fixture-probe" && plugin.state.status === "active"), "Probe plugin did not activate")
|
|
assert.ok(!(await client.plugin.list({ location })).data.some(plugin => plugin.id === "codenomad.automation"), "Location must be loaded before desktop plugins are installed")
|
|
if (reloadProvider) {
|
|
const { prepareReloadSafety } = await import("./fixtures/wsl-reload-safety.mjs")
|
|
reloadSafety = await prepareReloadSafety({ client, root, unc, provider: reloadProvider })
|
|
evidence.reloadSafety = { before: reloadSafety.before }
|
|
}
|
|
await prepareDesktopPluginPresence(paths, assertCurrent, { automation, pruning })
|
|
await delay(1_500)
|
|
evidence.beforeExplicitReload = (await client.plugin.list({ location })).data
|
|
.filter(plugin => plugin.id.startsWith("codenomad"))
|
|
.map(plugin => ({ id: plugin.id, status: plugin.state.status }))
|
|
// This explicitly tests the informed recovery action. It is never evidence
|
|
// of automatic watcher discovery; pre-reload native state is recorded above.
|
|
await client.location.reload({ signal: AbortSignal.any([signal, AbortSignal.timeout(15_000)]) })
|
|
if (reloadSafety) evidence.reloadSafety = await reloadSafety.observe()
|
|
await until(async request => {
|
|
const plugins = (await client.plugin.list({ location }, request)).data
|
|
evidence.plugins = plugins.filter(plugin => plugin.id.startsWith("codenomad") || plugin.id === "wsl-fixture-probe")
|
|
.map(plugin => ({ id: plugin.id, status: plugin.state.status }))
|
|
return ["codenomad.automation", "codenomad-session-pruning"].every(id => plugins.some(plugin => plugin.id === id && plugin.state.status === "active"))
|
|
}, "Late plugin discovery after native reload failed")
|
|
const changes = []
|
|
const watcher = mode === "native" ? undefined : watch(path.dirname(configFile), { recursive: true }, (event, filename) => changes.push({ event, filename }))
|
|
configWatcher = watcher
|
|
if (watcher) {
|
|
try {
|
|
const probe = path.join(path.dirname(configFile), ".watch-probe")
|
|
await writeFile(probe, "")
|
|
await rm(probe)
|
|
await until(async () => changes.some(change => change.filename === ".watch-probe"), "Windows config observer did not receive its control event")
|
|
} catch (error) { watcher.close(); throw error }
|
|
}
|
|
await delay(2_500)
|
|
const setupBefore = await readFile(unc(`${root}/plugin-setup.log`), "utf8")
|
|
const leasePaths = (await Promise.all(["automation", "session-pruning"].map(async feature => {
|
|
const directory = path.join(paths.data, feature, "presence")
|
|
return (await readdir(directory)).filter(name => name.endsWith(".lease")).map(name => path.join(directory, name))
|
|
}))).flat()
|
|
assert.equal(leasePaths.length, 2)
|
|
const leaseBefore = await Promise.all(leasePaths.map(async file => (await stat(file)).mtimeMs))
|
|
changes.length = 0
|
|
try {
|
|
// Windows ReadDirectoryChangesW cannot watch native WSL UNC directories.
|
|
// Observe Linux inotify there instead, including existing nested folders.
|
|
if (mode === "native") {
|
|
const events = await wsl(`python3 - ${quote(config)} ${quote(`${root}/runtime`)} <<'CODENOMAD_WATCH'
|
|
import ctypes, json, os, select, sys, time
|
|
libc = ctypes.CDLL(None, use_errno=True)
|
|
fd = libc.inotify_init1(os.O_NONBLOCK)
|
|
assert fd >= 0
|
|
# Prove this observer is live before measuring the real heartbeat interval.
|
|
control = libc.inotify_add_watch(fd, os.fsencode(sys.argv[2]), 0x00000fce)
|
|
assert control >= 0
|
|
probe = os.path.join(sys.argv[2], '.watch-probe')
|
|
open(probe, 'w').close()
|
|
os.unlink(probe)
|
|
assert select.select([fd], [], [], 2)[0]
|
|
os.read(fd, 1048576)
|
|
libc.inotify_rm_watch(fd, control)
|
|
os.read(fd, 1048576)
|
|
for directory, _, _ in os.walk(sys.argv[1]):
|
|
assert libc.inotify_add_watch(fd, os.fsencode(directory), 0x00000fce) >= 0
|
|
deadline = time.monotonic() + 5
|
|
events = []
|
|
while time.monotonic() < deadline:
|
|
if select.select([fd], [], [], max(0, deadline-time.monotonic()))[0]:
|
|
events.append(os.read(fd, 1048576).hex())
|
|
os.close(fd)
|
|
print(json.dumps(events))
|
|
CODENOMAD_WATCH`)
|
|
changes.push(...JSON.parse(events))
|
|
} else await delay(5_000)
|
|
} finally { watcher?.close() }
|
|
const leaseAfter = await Promise.all(leasePaths.map(async file => (await stat(file)).mtimeMs))
|
|
assert.ok(leaseAfter.every((time, index) => time > leaseBefore[index]), "Both backend leases must actually heartbeat")
|
|
assert.deepEqual(changes, [], "Heartbeats must leave the entire watched config tree unchanged")
|
|
assert.equal(await readFile(unc(`${root}/plugin-setup.log`), "utf8"), setupBefore, "Heartbeats must not rerun plugin setup")
|
|
assert.equal((await client.server.info()).pid, before.pid)
|
|
evidence.heartbeat = { durationMs: 5_000, leaseBefore, leaseAfter, changes, stableSetup: true }
|
|
evidence.nativeWatcherFailures = (await readFile(unc(`${root}/data/opencode/log/opencode.log`), "utf8"))
|
|
.split("\n").filter(line => line.includes('message="failed to subscribe"')).slice(-4)
|
|
await automation.stop(); await pruning.stop(); await shared.shutdown()
|
|
await startOrRestart(true)
|
|
const replacement = await shared.client(options, { deadlineAt })
|
|
evidence.restarted = { pid: (await replacement.server.info()).pid }
|
|
assert.notEqual(evidence.restarted.pid, before.pid)
|
|
assert.equal((await replacement.location.get({ location })).directory, location.directory)
|
|
evidence.passed = true
|
|
console.log(`PASS Windows→${distro} (${mode}): real service start/authentication/restart/reconnect, translated filesystem provisioning, late plugin discovery after explicit native reload and measured heartbeat stability; ${mounted}`)
|
|
}
|
|
try {
|
|
await boundedFixtureOperation(runFixture, deadlineAt, "WSL native fixture", signal)
|
|
} catch (error) {
|
|
evidence.error = error.message
|
|
throw error
|
|
} finally {
|
|
configWatcher?.close()
|
|
const cleanupDeadline = Date.now() + 20_000
|
|
const cleanups = [
|
|
["reload observer", () => reloadSafety?.dispose()], ["local provider", () => reloadProvider?.stop()],
|
|
["automation presence", () => automation?.stop()], ["pruning presence", () => pruning?.stop()],
|
|
["native client", () => shared?.shutdown()],
|
|
["isolated daemon", () => started ? wsl(`${quote(wrapper)} service stop`, true) : undefined],
|
|
]
|
|
const results = await Promise.allSettled(cleanups.map(([label, operation]) => boundedFixtureOperation(operation, cleanupDeadline, label)))
|
|
evidence.cleanup = results.map((result, index) => ({ operation: cleanups[index][0], status: result.status,
|
|
...(result.status === "rejected" ? { error: String(result.reason) } : {}) }))
|
|
await boundedFixtureOperation(() => writeFile(path.join(mounted, "evidence.json"), JSON.stringify(evidence, null, 2)), Date.now() + 5_000, "write evidence")
|
|
const failures = results.filter(result => result.status === "rejected").map(result => result.reason)
|
|
if (failures.length) throw new AggregateError(failures, "Isolated WSL fixture cleanup failed")
|
|
}
|