Files
kilocode/packages/kilo-vscode/tests/unit/run-controller.test.ts
T
marius-kilocode 52447ed2af fix(agent-manager): scope run-script state to the owning project
The RunScriptManager keys entries by worktreeId and the webview sends
"local" for whichever project's local context is selected, so in
multi-project mode two projects' local runs collided in one entry: a
second project's Run button stopped the first project's script, and the
state payload spread every project's run statuses into the active
project's push, rendering foreign local rows as running.

The provider now namespaces the local key as <projectId>:local at the
message boundary (ambient project scope, no webview protocol change),
RunController treats the qualified suffix as a local run (repo root cwd,
"local" branch label), each project's state payload is filtered to its
own worktrees plus its un-namespaced local key, and runStatus emissions
are routed with a projectId stamp so background-project statuses land in
their own webview store. ScriptTerminalManager keys inherit the same
qualification, and its view mapping already namespaces run terminals to
the matching project bucket.
2026-07-30 17:50:04 +02:00

84 lines
3.1 KiB
TypeScript

import { describe, expect, it } from "bun:test"
import { mkdtempSync, rmSync } from "node:fs"
import { tmpdir } from "node:os"
import * as path from "node:path"
import { RunController, type RunTaskConfig } from "../../src/agent-manager/run/controller"
import type { RunStatus } from "../../src/agent-manager/run/manager"
import type { RunTerminalDestination } from "../../src/agent-manager/run/destination"
const destination: RunTerminalDestination = "vscode"
function setup() {
const root = mkdtempSync(path.join(tmpdir(), "kilo-run-controller-"))
const configs: RunTaskConfig[] = []
const statuses: RunStatus[] = []
const controller = new RunController({
root: () => root,
state: () => undefined,
open: () => Promise.resolve(),
start: (config, done) => {
configs.push(config)
return Promise.resolve({ stop: () => done({ stopped: true }) })
},
post: (status) => statuses.push(status),
error: () => undefined,
log: () => undefined,
})
return { root, configs, statuses, controller, cleanup: () => rmSync(root, { recursive: true, force: true }) }
}
describe("RunController local key scoping", () => {
it("resolves a project-qualified local key to the repo root", async () => {
const { root, configs, controller, cleanup } = setup()
try {
await controller.configure()
await controller.run("prj-a:local", destination)
expect(configs).toHaveLength(1)
expect(configs[0]!.worktreeId).toBe("prj-a:local")
expect(configs[0]!.cwd).toBe(root)
expect(configs[0]!.branch).toBe("local")
} finally {
cleanup()
}
})
it("keeps two projects' local runs as independent entries", async () => {
const { statuses, controller, cleanup } = setup()
try {
await controller.configure()
await controller.run("prj-a:local", destination)
await controller.run("prj-b:local", destination)
// Starting B must not stop A: both entries report running.
const all = controller.state().runStatuses
expect(all.find((s) => s.worktreeId === "prj-a:local")?.state).toBe("running")
expect(all.find((s) => s.worktreeId === "prj-b:local")?.state).toBe("running")
// Stopping A leaves B untouched.
controller.stop("prj-a:local")
await new Promise((resolve) => setTimeout(resolve, 10))
const after = controller.state().runStatuses
expect(after.find((s) => s.worktreeId === "prj-a:local")?.state).not.toBe("running")
expect(after.find((s) => s.worktreeId === "prj-b:local")?.state).toBe("running")
expect(statuses.some((s) => s.worktreeId === "prj-a:local" && s.state === "stopping")).toBe(true)
} finally {
cleanup()
}
})
it("still treats the legacy unqualified local key as local", async () => {
const { root, configs, controller, cleanup } = setup()
try {
await controller.configure()
await controller.run("local", destination)
expect(configs[0]!.worktreeId).toBe("local")
expect(configs[0]!.cwd).toBe(root)
expect(configs[0]!.branch).toBe("local")
} finally {
cleanup()
}
})
})