mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-10-25 15:51:21 +00:00
## Summary Follow-up to merged #835: fix the demonstrated dependency behind real `Instance event relay routing timed out` disconnects, not duplicate connection notifications. Existing diagnostics captured relay ownership blocked in `getWorktrees -> WorktreeInventory -> POST /api/worktree/refresh`, including a refresh pending beyond 150 seconds and a separate ~220-second completed call. That exceeds the unchanged 60-second relay job deadline, so the shared relay reconnects and Usage correctly clears for all projects. Event authorization now uses a separate **registered-only** native worktree inventory and never starts or joins native strategy discovery. Native project/checkout checks, physical Git membership, clone exclusion, configured roots, nested-folder and WSL projection remain in the shared catalogue implementation. Native/local invalidation and workspace disposal fence both inventories and their distinct directory-cache namespaces. Ordinary request authorization and explicit worktree discovery retain refresh and read-your-writes behavior. Full-location checks receive the connection attempt's abort signal. No timeout increase, swallowed reconnect, stale authorization fallback or quota-specific workaround. FIFO lanes, ownership retries, generation/workspace fences and retained-work budgets are unchanged. ## Validation - Baseline-red production-manager + relay reproduction: hold discovery unresolved and require the linked-worktree event to route before releasing it. - All **248 workspace tests pass**, including event order/reconnect, ownership, no-Git, WSL, configured Git roots and inventory invalidation. - Server TypeScript check passes. - Isolated OpenCode **2.0.22** location/worktree/event fixture passes, including real pre-refresh native registration and ordered real session events during a held fixture-manager discovery request. - Isolated no-Git native prompt/recovery fixture passes. - `git diff --check` passes. ## Scope / limitations The native refresh's internal delay is not claimed fixed; the relay no longer depends on that unnecessary discovery operation. Actual transport failures and other stuck native reads still trigger bounded reconnect/reconciliation. Compaction and window-resize jank are not established causes or fixed by this PR. The installed application, shared daemon, user settings and independent #824 worktree are untouched. This conversation remains attached to `D:\CodeNomad`. Existing oversized source touched: `packages/server/src/workspaces/manager.ts`, approximately 1,260 lines. No unrelated size-only refactor. Independent Gatekeeper review will be published against the exact PR head, with findings resolved and fresh passes until zero findings before the requested administrator merge.
181 lines
11 KiB
JavaScript
181 lines
11 KiB
JavaScript
import assert from "node:assert/strict"
|
|
import { execFileSync } from "node:child_process"
|
|
import { mkdir, writeFile, rm, realpath } from "node:fs/promises"
|
|
import path from "node:path"
|
|
import { tsImport } from "tsx/esm/api"
|
|
import pino from "pino"
|
|
import { setTimeout as delay } from "node:timers/promises"
|
|
|
|
// Invoked by the isolated daemon fixture only. No service discovery/user state.
|
|
export async function testNativeWorktreeManagement({ client, root }) {
|
|
const { listNativeWorktrees, createNativeWorktree, removeNativeWorktree } = await tsImport("../packages/server/src/workspaces/native-worktrees.ts", import.meta.url)
|
|
const { WorktreeInventory } = await tsImport("../packages/server/src/workspaces/worktree-inventory.ts", import.meta.url)
|
|
const repo = path.join(root, "worktree-policy")
|
|
const external = path.join(root, "agent-created")
|
|
const clone = path.join(root, "independent-clone")
|
|
await mkdir(repo)
|
|
const git = (directory, ...args) => execFileSync("git", ["-C", directory, ...args], { encoding: "utf8" }).trim()
|
|
git(repo, "init", "-b", "main")
|
|
git(repo, "config", "user.name", "CodeNomad fixture")
|
|
git(repo, "config", "user.email", "fixture@example.invalid")
|
|
await mkdir(path.join(repo, "packages", "app"), { recursive: true })
|
|
await writeFile(path.join(repo, "packages", "app", "base.txt"), "base")
|
|
git(repo, "add", ".")
|
|
git(repo, "commit", "-m", "base")
|
|
git(repo, "worktree", "add", "-b", "agent", external)
|
|
await writeFile(path.join(external, "source.txt"), "selected checkout")
|
|
git(external, "add", ".")
|
|
git(external, "commit", "-m", "selected checkout")
|
|
const sourceHead = git(external, "rev-parse", "HEAD")
|
|
await writeFile(path.join(external, "uncommitted.txt"), "stay here")
|
|
git(root, "clone", "--no-hardlinks", repo, clone)
|
|
const location = await client.location.get({ location: { directory: repo } })
|
|
await client.location.get({ location: { directory: external } })
|
|
await client.location.get({ location: { directory: clone } })
|
|
const context = { client, location, workspacePath: repo, toHost: async directory => directory }
|
|
const registered = await listNativeWorktrees(context, { refresh: false })
|
|
assert.equal(registered.worktrees.length, 2, "native event locations must be registered without strategy refresh; shared-project clones remain excluded")
|
|
let catalogue = await listNativeWorktrees(context)
|
|
assert.equal(catalogue.worktrees.length, 2, "same-project independent clone must be excluded")
|
|
const source = catalogue.worktrees.find(entry => entry.branch === "agent")
|
|
assert.ok(source)
|
|
const created = await createNativeWorktree(context, "feature/ui", source.slug)
|
|
const expectedParent = path.join(await realpath(repo), ".codenomad", "worktrees")
|
|
assert.equal(path.dirname(created.registeredDirectory), expectedParent)
|
|
assert.equal(git(created.registeredDirectory, "symbolic-ref", "--short", "HEAD"), "feature/ui")
|
|
assert.equal(git(created.registeredDirectory, "rev-parse", "HEAD"), sourceHead)
|
|
assert.equal(git(created.registeredDirectory, "status", "--porcelain"), "")
|
|
assert.match(git(external, "status", "--porcelain"), /uncommitted/)
|
|
assert.ok(git(repo, "check-ignore", created.registeredDirectory))
|
|
git(created.registeredDirectory, "branch", "-m", "feature/renamed")
|
|
catalogue = await listNativeWorktrees(context)
|
|
assert.equal(catalogue.worktrees.find(entry => entry.branch === "feature/renamed").slug, created.slug)
|
|
await assert.rejects(createNativeWorktree(context, "agent"), "Git must refuse a branch already checked out")
|
|
assert.equal((await listNativeWorktrees(context)).worktrees.length, 3, "failed branch attachment must clean up its new checkout")
|
|
const nested = path.join(repo, "packages", "app")
|
|
const nestedCatalogue = await listNativeWorktrees({ ...context, workspacePath: nested, location: { directory: nested } })
|
|
assert.equal(nestedCatalogue.worktrees.find(entry => entry.slug === created.slug).directory, path.join(created.registeredDirectory, "packages", "app"))
|
|
await writeFile(path.join(created.registeredDirectory, "dirty.txt"), "keep")
|
|
await assert.rejects(removeNativeWorktree(context, created.serviceRoot, false))
|
|
await rm(path.join(created.registeredDirectory, "dirty.txt"))
|
|
await removeNativeWorktree(context, created.serviceRoot, false)
|
|
assert.equal(git(repo, "rev-parse", "refs/heads/feature/renamed"), sourceHead, "removal must preserve the branch")
|
|
assert.equal((await listNativeWorktrees(context)).worktrees.length, 2)
|
|
const detached = [path.join(root, "detached-one"), path.join(root, "detached-two")]
|
|
for (const directory of detached) git(repo, "worktree", "add", "--detach", directory, "HEAD")
|
|
const detachedEntries = (await listNativeWorktrees(context)).worktrees.filter(entry => !entry.branch)
|
|
assert.equal(detachedEntries.length, 2)
|
|
assert.notEqual(detachedEntries[0].slug, detachedEntries[1].slug)
|
|
const fromLinked = await listNativeWorktrees({ ...context, workspacePath: external, location: { directory: external } })
|
|
assert.equal(fromLinked.defaultDirectory, catalogue.defaultDirectory, "opening a linked checkout must not nest the default parent")
|
|
assert.equal(fromLinked.worktrees.find(entry => entry.branch === "main").removable, false)
|
|
let scans = 0
|
|
const changed = []
|
|
const inventory = new WorktreeInventory({
|
|
load: async () => { assert.ok(++scans < 8, "native refresh events must not cause a scan loop"); return listNativeWorktrees(context) },
|
|
changed: id => changed.push(id),
|
|
failed: (_id, error) => { throw error },
|
|
})
|
|
const controller = new AbortController()
|
|
let connected
|
|
const ready = new Promise(resolve => { connected = resolve })
|
|
const events = (async () => {
|
|
for await (const event of client.event.subscribe({ signal: controller.signal })) {
|
|
if (event.type === "server.connected") connected()
|
|
if (event.type === "worktree.updated") inventory.invalidate()
|
|
}
|
|
})()
|
|
try {
|
|
await ready
|
|
const cached = await inventory.read("fixture")
|
|
assert.equal(await inventory.read("fixture"), cached)
|
|
assert.equal(scans, 1, "sequential display reads must reuse the completed native scan")
|
|
git(external, "branch", "-m", "agent-renamed")
|
|
inventory.invalidate("fixture")
|
|
assert.equal(scans, 1, "invalidation must stay lazy")
|
|
assert.equal(await inventory.read("fixture"), cached, "display must not wait for background Git")
|
|
const updated = await inventory.read("fixture", "validated")
|
|
assert.equal(updated.worktrees.find(entry => entry.slug === source.slug).branch, "agent-renamed")
|
|
assert.deepEqual(changed, ["fixture"])
|
|
assert.equal(scans, 2)
|
|
await inventory.read("fixture", "fresh")
|
|
assert.equal(scans, 3, "family validation must bypass a warm display cache")
|
|
assert.deepEqual(changed, ["fixture"], "an unchanged scan must not create a reload feedback loop")
|
|
console.log("PASS: native worktree cache reuse, lazy rename refresh and forced family validation")
|
|
} finally {
|
|
controller.abort()
|
|
await events.catch(error => { if (!controller.signal.aborted) throw error })
|
|
}
|
|
const { WorkspaceManager } = await tsImport("../packages/server/src/workspaces/manager.ts", import.meta.url)
|
|
const { EventBus } = await tsImport("../packages/server/src/events/bus.ts", import.meta.url)
|
|
const { InstanceEventBridge } = await tsImport("../packages/server/src/workspaces/instance-events.ts", import.meta.url)
|
|
const bus = new EventBus()
|
|
let holdRefresh = false, refreshEntered = false, releaseRefresh
|
|
const heldRefresh = new Promise(resolve => { releaseRefresh = resolve })
|
|
const managerClient = { ...client, worktree: { ...client.worktree, refresh: async (...args) => {
|
|
if (holdRefresh) { refreshEntered = true; await heldRefresh }
|
|
return client.worktree.refresh(...args)
|
|
} } }
|
|
// Exercise the production manager's post-mutation invalidation using only
|
|
// this fixture's authenticated client. No native discovery/user service.
|
|
const manager = new WorkspaceManager({
|
|
rootDir: root,
|
|
settings: { getOwner: () => ({ environmentVariables: {} }) },
|
|
binaryResolver: { resolveDefault: () => ({ path: process.execPath, label: "Isolated fixture" }) },
|
|
eventBus: bus,
|
|
logger: pino({ level: "silent" }),
|
|
sharedService: {
|
|
client: async () => managerClient,
|
|
headers: async () => ({}),
|
|
validateLocation: async location => client.location.get({ location }),
|
|
subscribe: async options => client.event.subscribe(options),
|
|
shutdown: async () => {},
|
|
},
|
|
})
|
|
let bridge, pendingScan, eventSession
|
|
try {
|
|
const { workspace } = await manager.create(repo)
|
|
const before = await manager.getWorktrees(workspace.id)
|
|
const statuses = [], renames = []
|
|
bus.on("instance.eventStatus", event => statuses.push(event.status))
|
|
bus.on("instance.event", ({ event }) => { if (event.type === "session.renamed") renames.push(event.data.title) })
|
|
bridge = new InstanceEventBridge({ workspaceManager: manager, eventBus: bus, logger: pino({ level: "silent" }) })
|
|
const until = async predicate => {
|
|
const deadline = Date.now() + 10_000
|
|
while (!predicate()) { assert.ok(Date.now() < deadline, "native event ownership joined the held discovery refresh"); await delay(10) }
|
|
}
|
|
bus.publish({ type: "workspace.started", workspace })
|
|
await until(() => statuses.includes("connected"))
|
|
holdRefresh = true
|
|
pendingScan = manager.getWorktrees(workspace.id, "fresh")
|
|
await until(() => refreshEntered)
|
|
// This location and its events are real native registrations, not a stub
|
|
// inventory. Only this isolated manager's discovery request is held.
|
|
eventSession = await client.session.create({ location: { directory: external } })
|
|
await client.session.update({ sessionID: eventSession.id, title: "held-discovery-event-1" })
|
|
await client.session.update({ sessionID: eventSession.id, title: "held-discovery-event-2" })
|
|
await until(() => renames.length === 2)
|
|
assert.deepEqual(renames, ["held-discovery-event-1", "held-discovery-event-2"])
|
|
assert.equal(statuses.includes("error"), false)
|
|
assert.equal(await manager.ownsDirectory(workspace.id, clone, "event"), false)
|
|
releaseRefresh(); holdRefresh = false; await pendingScan
|
|
bridge.shutdown()
|
|
console.log("PASS: production event relay routes ordered native worktree events while a separate discovery refresh is stalled")
|
|
const added = await manager.createWorktree(workspace.id, "cached-create")
|
|
const afterCreate = await manager.getWorktrees(workspace.id)
|
|
assert.equal(afterCreate.worktrees.length, before.worktrees.length + 1)
|
|
assert.ok(afterCreate.worktrees.some(entry => entry.slug === added.slug), "create-and-use must receive the new ID immediately")
|
|
await manager.removeWorktree(workspace.id, added.serviceRoot, false)
|
|
const afterRemove = await manager.getWorktrees(workspace.id)
|
|
assert.equal(afterRemove.worktrees.length, before.worktrees.length)
|
|
assert.ok(!afterRemove.worktrees.some(entry => entry.slug === added.slug), "post-delete refresh must not resurrect a cached checkout")
|
|
console.log("PASS: production manager warm-cache create/remove read-your-writes")
|
|
} finally {
|
|
bridge?.shutdown()
|
|
releaseRefresh()
|
|
await pendingScan
|
|
if (eventSession) await client.session.remove({ sessionID: eventSession.id })
|
|
await manager.shutdown()
|
|
}
|
|
console.log("PASS: native worktree discovery/create/remove, clone scope, selected HEAD, default parent, named branches, stable identity, nested paths and dirty/checked-out guards")
|
|
}
|