mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
a19d44c3ef
* fix(cli): advertise the instance from enableRemote so /remote registers as a spawn target Enabling the remote relay from the TUI `/remote` slash command connected the socket and mirrored sessions, but never advertised the instance, so the CLI never appeared as a spawn target in the mobile "Run on" picker. Only the explicit `kilo remote` command called setInstanceAdvertisement. The advertisement now runs on every successful enableRemote() entry, before the already-connected and coalescing early returns. That ordering matters: bootstrap auto-enable frequently connects first, so `/remote` usually hits `if (remote) return` and an advertisement placed in the connection-setup body would leave the defect unfixed in the common case. `ingestDisabled` returns before the advertisement and stays unadvertised. The ensure helper is a no-op when an advertisement is already set, so it fires no extra heartbeat, while explicit setInstanceAdvertisement keeps its existing replace semantics. buildInstanceAdvertisement moves to a shared module so the command path and the enable path derive it identically. * fix(cli): report pending question and permission on the session heartbeat The heartbeat built each session's status from SessionStatus.Service, whose union is idle/retry/busy/offline and which never consults Question.Service or Permission.Service. deriveStatus() already did consult both, but only fed the ingest session_status sync. So a session genuinely blocked on a question was advertised as busy on the heartbeat, and the mobile app — which takes live row status from the heartbeat — showed no needs-input badge. Extract the precedence (permission, then question, then SessionStatus) into a shared helper used by both deriveStatus and the heartbeat, so the two channels cannot drift. The heartbeat runs on a ~10s timer across every session, and deriveStatus makes service calls per session, so the permission and question lists are fetched once per tick and indexed by session id rather than queried per session. A test pins the call count. Behaviour note beyond the strict fix: sharing the derivation also means a SessionStatus of offline now reports as retry on the wire, matching what deriveStatus has always sent to ingest. Nothing consumes offline from the heartbeat — the transport forwards only idle and busy, and the mobile row treats both as non-attention — so the effect is that the two channels now agree. The detach fence test is parameterised accordingly; its assertion that the status clears on detach is unchanged. * chore(cli): widen the promise-facade allowlist for the heartbeat attention tests The DEF-3 heartbeat tests raise and reply to real Question and Permission requests through the global AppRuntime, which took kilo-sessions.test.ts from 4 classified references to 29 and failed the allowlist check. Bumping the count rather than restructuring the tests is deliberate: the heartbeat resolves attention status from the global Question.Service and Permission.Service, so asserting it requires driving those same services. Scoped layers cannot express that — the global-runtime coupling is the thing under test — and it is the same integration pattern this entry already sanctioned for the detach fence. The reason string records that.
133 lines
6.5 KiB
TypeScript
133 lines
6.5 KiB
TypeScript
#!/usr/bin/env bun
|
|
// kilocode_change - new file
|
|
|
|
/**
|
|
* Prevents new service-local runtimes in shared Effect modules while the
|
|
* remaining Kilo Promise facades are migrated away. It also prevents tests
|
|
* from reaching through the global application runtime unless the integration
|
|
* boundary is explicitly classified.
|
|
*
|
|
* Existing sites are allowed only when classified below. Remove transitional
|
|
* entries after their migration lands so later reintroductions fail CI.
|
|
*/
|
|
|
|
import path from "node:path"
|
|
|
|
const ROOT = path.resolve(import.meta.dir, "..")
|
|
const DIR = path.join(ROOT, "packages", "opencode", "src")
|
|
const TEST_DIR = path.join(ROOT, "packages", "opencode", "test")
|
|
const PATTERN = /makeRuntime\s*\(\s*Service\s*,/g
|
|
const TEST_PATTERN = /\bAppRuntime\b/g
|
|
|
|
const allow: Record<string, string> = {
|
|
"bus/index.ts": "core bus callback and synchronous runtime boundary",
|
|
"cli/cmd/run/runtime.boot.ts": "direct run startup resolver runtime boundary",
|
|
"cli/cmd/run/stream.transport.ts": "per-subscription direct run transport runtime boundary",
|
|
"cli/cmd/run/variant.shared.ts": "direct run variant persistence runtime boundary with test filesystem injection",
|
|
"config/tui.ts": "separately tracked TUI config facade moved by the upstream TUI extraction",
|
|
"installation/index.ts": "existing installation facade outside #10655",
|
|
}
|
|
|
|
const testAllow: Record<string, { count: number; reason: string }> = {
|
|
"preload.ts": { count: 2, reason: "global test-suite AppRuntime cleanup boundary" },
|
|
"kilocode/config-resilience.test.ts": { count: 4, reason: "existing runtime integration test" },
|
|
"kilocode/config-validation.test.ts": { count: 2, reason: "existing runtime integration test" },
|
|
"kilocode/cli-shutdown.test.ts": { count: 1, reason: "mocked runtime boundary for shutdown unit tests" },
|
|
"kilocode/plan-followup.test.ts": { count: 3, reason: "existing runtime integration test" },
|
|
"kilocode/session-compaction-chunks.test.ts": {
|
|
count: 2,
|
|
reason: "disk-backed instance integration test cleanup",
|
|
},
|
|
"kilocode/session-fork-remap.test.ts": {
|
|
count: 2,
|
|
reason: "disk-backed instance integration test cleanup",
|
|
},
|
|
"kilocode/kilo-sessions.test.ts": {
|
|
count: 29,
|
|
reason:
|
|
"K1 W1: real integration test for SessionStatus→detach→heartbeat-fence; " +
|
|
"the test creates a session and sets its status via the global AppRuntime, " +
|
|
"then drives the module-level KiloSessions seams and verifies the fence. " +
|
|
"DEF-3 extends this with heartbeat attention-status coverage: the heartbeat " +
|
|
"resolves pending question/permission from the global Question.Service and " +
|
|
"Permission.Service, so a test can only assert it by raising and replying to " +
|
|
"real requests through that same runtime. Scoped layers cannot express this — " +
|
|
"the global-runtime coupling is exactly what is under test.",
|
|
},
|
|
"kilocode/session/platform-attribution.test.ts": { count: 2, reason: "existing runtime integration test" },
|
|
"kilocode/session-prompt-queue.test.ts": { count: 6, reason: "prompt queue legacy instance bridge regression" },
|
|
"server/experimental-session-list.test.ts": { count: 2, reason: "Kilo session list integration test" },
|
|
"kilocode/server/cloud-session-import.test.ts": { count: 5, reason: "full app cloud import transaction integration" },
|
|
"kilocode/server/listener-runtime.test.ts": { count: 4, reason: "listener and AppRuntime integration test" },
|
|
"tool/recall.test.ts": { count: 11, reason: "existing runtime integration test" },
|
|
}
|
|
|
|
const owned = (file: string) => file.startsWith("kilocode/") || file.startsWith("kilo-sessions/")
|
|
const hits: Array<{ file: string; line: number }> = []
|
|
const glob = new Bun.Glob("**/*.ts")
|
|
|
|
for (const file of glob.scanSync({ cwd: DIR, onlyFiles: true })) {
|
|
if (owned(file)) continue
|
|
const text = await Bun.file(path.join(DIR, file)).text()
|
|
for (const match of text.matchAll(PATTERN)) {
|
|
const line = text.slice(0, match.index ?? 0).split("\n").length
|
|
hits.push({ file, line })
|
|
}
|
|
}
|
|
|
|
const invalid = hits.filter((hit) => !allow[hit.file])
|
|
const drift = Object.entries(allow).flatMap(([file, reason]) => {
|
|
const count = hits.filter((hit) => hit.file === file).length
|
|
if (count === 1) return []
|
|
return [` packages/opencode/src/${file}: expected 1 classified site, found ${count} (${reason})`]
|
|
})
|
|
|
|
const testHits: Array<{ file: string; line: number }> = []
|
|
for (const file of glob.scanSync({ cwd: TEST_DIR, onlyFiles: true })) {
|
|
const text = await Bun.file(path.join(TEST_DIR, file)).text()
|
|
for (const match of text.matchAll(TEST_PATTERN)) {
|
|
const line = text.slice(0, match.index ?? 0).split("\n").length
|
|
testHits.push({ file, line })
|
|
}
|
|
}
|
|
|
|
const testInvalid = testHits.filter((hit) => !testAllow[hit.file])
|
|
const testDrift = Object.entries(testAllow).flatMap(([file, entry]) => {
|
|
const count = testHits.filter((hit) => hit.file === file).length
|
|
if (count === entry.count) return []
|
|
return [
|
|
` packages/opencode/test/${file}: expected ${entry.count} classified reference(s), found ${count} (${entry.reason})`,
|
|
]
|
|
})
|
|
|
|
if (invalid.length > 0 || drift.length > 0 || testInvalid.length > 0 || testDrift.length > 0) {
|
|
if (invalid.length > 0) {
|
|
console.error("Found unclassified service-local Effect runtimes in shared opencode modules:")
|
|
for (const hit of invalid) console.error(` packages/opencode/src/${hit.file}:${hit.line}`)
|
|
console.error("")
|
|
}
|
|
if (drift.length > 0) {
|
|
console.error("Classified service-local runtime exceptions no longer match the current source:")
|
|
for (const item of drift) console.error(item)
|
|
console.error("")
|
|
}
|
|
if (testInvalid.length > 0) {
|
|
console.error("Found unclassified AppRuntime use in opencode tests:")
|
|
for (const hit of testInvalid) console.error(` packages/opencode/test/${hit.file}:${hit.line}`)
|
|
console.error("")
|
|
}
|
|
if (testDrift.length > 0) {
|
|
console.error("Classified test AppRuntime exceptions no longer match the current source:")
|
|
for (const item of testDrift) console.error(item)
|
|
console.error("")
|
|
}
|
|
console.error("Do not add Promise facades to shared Effect services or global AppRuntime dependencies to tests.")
|
|
console.error("Yield services directly in scoped layers, or classify intentional integration boundaries explicitly.")
|
|
console.error("Remove migrated exceptions, or classify intentional runtime changes with an explicit reason.")
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log(
|
|
`check-opencode-promise-facades: ${hits.length} classified runtime site(s), ${testHits.length} classified test reference(s), no runtime drift found.`,
|
|
)
|