Files
kilocode/script/check-opencode-promise-facades.ts
T
Igor Šćekić a19d44c3ef Advertise the instance from enableRemote and report attention status on the heartbeat (#12532)
* 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.
2026-07-25 15:45:43 +02:00

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.`,
)