Files
kilocode/packages/opencode/test/server/httpapi-sdk.test.ts
T
Johnny Eric Amancio ef6b152ff8 OpenCode v1.16.2 (#12088)
* feat(worktree): add managed workspace cloning (#30117)

* test(tui): skip crashing keymap textarea renderer

* fix(core): allow skipping migration execution

* fix(opencode): remove automatic full session diffs (#30127)

* chore: generate

* refactor(worktree): move project out of repository

* zen: deepseek flash

* fix(tui): remount session view on session switch (#30129)

Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>

* go: minimax m3

* refactor(opencode): inline local provider helpers (#30169)

* refactor(opencode): simplify provider setup flow (#30173)

* fix(app): show project sessions before path sync resolves (#30167)

Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>

* fix(core): preserve session metadata migration identity (#30176)

* refactor(session): align namespace imports and inline trivial helpers (#30180)

* opencode(run): add queued prompt management (#30103)

Direct run mode previously made submitted follow-up prompts irrevocable while a response was still running. Let users edit or remove queued prompts before dispatch without interrupting the active turn.

* chore: generate

* fix(acp): honor session/cancel by aborting the running turn (#30145)

Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>

* fix(tui): prevent prompt corruption when pasting near wide characters (#29710)

Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Simon Klee <hello@simonklee.dk>

* fix(opencode): avoid nullable webfetch format schema (#30215)

* chore: generate

* fix(core): contain lsp warmup defects (#30226)

* add run --replay mode (#30239)

* chore: generate

* chore: update nix node_modules hashes

* fix(stats): restore leaderboard spacing

* fix(stats): center top models dot grid

* fix(stats): stabilize top models hover

* fix(stats): align big-pickle provider resolution (#30274)

* feat(app): v2 desktop UI improvements (#29689)

Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>

* chore: generate

* fix(tui): clarify inline subagent rows (#30051)

* fix(tui): handle events across workspaces (#30281)

* feat(core): update Copilot for token-based billing (#30181)

* fix(tui): keep background marker with subagent label (#30271)

* fix(tui): keep retry attempt before message (#30275)

* chore: generate

* fix(opencode): enforce storage path invariants (#29666)

* chore: generate

* feat(core): add location-based permission service (#30287)

* chore: generate

* fix(tui): preserve live parts during session hydration (#30300)

* fix(app): restore deferred MCP status updates (#30220)

* fix: export v2 stylesheets and declare core node types (#30312)

* chore: update nix node_modules hashes

* fix(app): avoid suspending on pending child path (#30314)

* fix(opencode): remove sunsetted gpt-5.2 and gpt-5.3-codex from allowed models for codex subscriptions (#30316)

* chore: generate

* feat(core): expose session location

* chore: generate

* fix(opencode): preserve websocket api errors (#30321)

* refactor(core): simplify session pagination

* feat(core): add location filesystem contract

* feat(core): add dummy location filesystem layer

* chore: generate

* feat(opencode): add filesystem read and list routes

* chore: generate

* infra: stats

* sync

* feat(app): inset new layout session panels (#30342)

* fix(app): tab title truncation and close button positioning (#30349)

* tui: show model context in run footer (#30380)

* tui: revert OpenTUI upgrade to 0.2.16 (#30383)

* chore: update nix node_modules hashes

* feat(core): add managed repository cache (#30408)

* chore: generate

* chore: generate

* sync

* feat(stats): add cache ratio section

* feat(core): add flagged project references (#30414)

* chore: generate

* feat(core): support named migrations (#30418)

* fix(stats): clean retired provider rows during sync (#30420)

* fix(stats): mention opencode go in top models copy

* feat(core): expose project reference filesystem access (#30423)

* chore: generate

* sync

* fix(tui): scope diff viewer to session directory (#30426)

* test: widen provider header timeout margin (#30427)

* fix(plugin): restore private git install fallback (#30430)

* fix(stats): remove leaderboard nav link

* chore(opencode): remove scout agent (#30435)

* chore: generate

* feat(stats): improve cache ratio chart

* chore: generate

* fix(effect-drizzle-sqlite): preserve transaction begin errors (#30448)

* chore: bump effect beta to 74 (#30449)

* Revert "tui: revert OpenTUI upgrade to 0.2.16 (#30383)" (#30452)

* chore: update nix node_modules hashes

* refactor(opencode): improve startup time by 38% (#30453)

Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>

* chore: generate

* fix(opencode): patch empty Gemini replay messages (#30463)

* chore: generate

* refactor(core): consolidate filesystem services (#30447)

* chore: generate

* run: enable interactive replay by default (#30465)

* chore: update nix node_modules hashes

* refactor(opencode): remove JSON storage migration (#30461)

* chore: generate

* chore: update nix node_modules hashes

* fix(tui): stop idle background task spinner (#30484)

* refactor(core): move v1 schemas into core (#30473)

* chore: generate

* fix: task id passed to background job for continuation (#30485)

* chore: generate

* feat(core): project copying and tracking directories (#30139)

* chore: generate

* fix(opencode): preserve signed thinking during anthropic reorder (#30182)

* Revert "fix(opencode): preserve signed thinking during anthropic reorder" (#30502)

* fix: rm tool reorder logic from old bug (#30483)

* chore: generate

* feat(app): polish home projects list UI (#30436)

* feat(app): polish select-v2 component (#30446)

Co-authored-by: Brendan Allan <git@brendonovich.dev>

* fix(github): enforce existing git author identity (#30507)

* feat(app): new update button  (#30460)

Co-authored-by: Brendan Allan <git@brendonovich.dev>

* fix(opencode): fallback to sh for curl upgrade (#30499)

Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>

* fix(ui): render whole-file patches as complete diffs (#30516)

* chore: generate

* feat(app): add servers tab to settings dialog (#29675)

* refactor(core): consolidate pty service (#30537)

* chore: generate

* tui: truncate sidebar file paths (#30531)

* chore: update nix node_modules hashes

* feat(stats): add geo breakdown (#30456)

* chore: generate

* chore: update nix node_modules hashes

* fix(acp): classify apply_patch as edit (#30564)

* fix(acp): classify task as think (#30565)

* fix(acp): include external directory permission context (#30567)

* fix(acp): clean read tool display content (#30569)

* fix(tui): route question responses by session directory (#30578)

* fix(stats): serve stats og image from banner

* docs(go): add Qwen3.7 Plus model (#30594)

* fix(openai): preserve websocket idle state (#30586)

* refactor(core): remove ai sdk option fields (#30581)

* chore: generate

* test(core): cover v1 provider option lowering (#30599)

* chore: generate

* refactor(core): nest model api id (#30603)

* fix(core): expose azure openai xhigh efforts (#30620)

* feat(core): add skill registry and file agent loading (#30617)

* chore: generate

* chore: update nix node_modules hashes

* fix(stats): count all go usage

* chore: remove zed extension and automation (#30628)

* fix(opencode): preserve variant for delegated tasks (#30630)

* zen: update nvidia tos

* fix(opencode): route SAP AI Core reasoning variants through modelParams (#30482)

* chore: generate

* fix(app): hide unavailable titlebar update (#30642)

* feat(app): v2 thinking level selector (#30646)

* fix(app,ui): session review reactivity and VCS query cache (#30660)

* feat(core): add embedded v2 session runtime and tool foundation (#30632)

* chore: generate

* chore: update nix node_modules hashes

* docs: correct compaction prune default (#30670)

* fix(opencode): avoid shell cancel race (#30641)

* feat: bump bedrock and add proper mantle support for openai models through aws bedrock (#30464)

* test: wait for shell truncation readiness (#30679)

* chore: update nix node_modules hashes

* refactor(opencode): clean up task tool prompts (#30687)

* feat(core): add command registry (#30624)

* chore: generate

* fix(acp): replay loaded session transcript (#30645)

Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>

* fix(core): reset pre-launch session projections (#30728)

* feat(tui): improve experimental session switcher (#30738)

* fix(opencode): respect disabled auto compaction on overflow (#30749)

* zen: nemotron 3 ultra

* fix(enterprise): install hono standard validator peer (#30740)

Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>

* fix build

* chore: update nix node_modules hashes

* make scripts executable

* fix(tui): show toast when variant_list keybind used with no variants (#30724)

* fix(opencode): `ACP.loadSession` should replay all messages (#30761)

Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>

* fix(opencode): attribute task child agent on creation (#30786)

* fix(tui): add Vue syntax highlighting (#30802)

* fix: bump @openrouter/ai-sdk-provider to 2.9.0 (#30800)

* feat(core): moving sessions (#30640)

* chore: generate

* tweak: background agent prompting to avoid polling issues (#30790)

* upgrade opentui to 0.3.2 (#30748)

* chore: update nix node_modules hashes

* feat(desktop): surface local server startup failures (#30822)

* ci: publish

* refactor(core): make v2 session inputs event sourced (#30785)

* chore: generate

* fix(llm): normalize OpenAI function tool schemas

* chore: generate

* feat(stats): refresh stats routes and homepage (#30419)

* fix(stats): sort metric charts by top usage

* feat(core): add public native API (#30828)

* chore: generate

* feat(app): color themes (#30824)

Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>

* chore: generate

* sync release versions for v1.16.0

* feat(core): attach global native tools (#30832)

* chore: generate

* feat(core): add Snowflake Cortex provider (#29901)

Co-authored-by: Cortex Code <noreply@snowflake.com>

* chore: generate

* feat(core): persist v2 session context epochs (#30789)

* chore: generate

* feat(tui): allow backgrounding synchronous subagents (#30488)

* fix(app): improve tab handling (#30669)

* chore: generate

* fix(tui): prioritize models slash autocomplete (#30848)

* fix(tui): route permission replies to session directory (#30851)

Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>

* fix(cli): harden daemon lifecycle (#30844)

* chore: generate

* feat(app): improve desktop multi-server support (#30678)

Co-authored-by: Brendan Allan <git@brendonovich.dev>

* chore: generate

* fix(app): handle tab overflow and scrolling in titlebar (#30886)

* fix(app): tab overflow (#30894)

* tui: guard path formatting inputs (#30469)

Fixes #27726, #25216, #24856, #24294, #17071, #29164, #24837, #16865, #14279, #29895

* opencode/run: refresh themes after terminal reloads (#30917)

* chore: generate

* fix(tui): fall back to local cwd when editor spawns in attach mode (#30583)

* docs: update Go Qwen tiered pricing (#30936)

* chore: generate

* feat(tui): add diff hunk navigation (#30935)

* chore: rm fuzzy search on references (#30931)

* fix: use mapError instead of orDie for context snapshot decoding (#30905)

Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>

* fix(core): recover corrupted models cache (#30947)

* chore: bun install (#30968)

* fix(opencode): resolve Bedrock hang by using node build conditions (#30873)

* fix(workflows): retry nix-hashes compute-hash on transient failure (#30743)

* fix(stats): scroll model charts to latest on mobile

* fix(opencode): prevent destructive edit matches (#30932)

* chore: generate

* fix(core): respect v2 default agents (#30969)

* chore: generate

* test(opencode): remove disposal event wait race (#30971)

* test(opencode): remove shell timeout output race (#30974)

* fix(opencode): gate reasoning summaries by provider (#30973)

* feat(core): admit v2 skill guidance (#30843)

* fix(workflows): serialize desktop release uploads (#30978)

* fix(stats): add mobile chart end spacing

* release: v1.16.2

* refactor: kilo compat for v1.16.2

* fix(opencode): address v1.16.2 merge regressions

* chore: update kilo-vscode visual regression baselines

* fix(opencode): restore Kilo behavior after v1.16.2 merge

* fix(opencode): retry Windows migration cleanup

* test(opencode): restore clone and macOS watcher coverage

* fix(opencode): address second-pass review for #12099

Preserve imported usage and retry partial JSON migrations. Refresh active dependency patches, remove the obsolete GCP patch, and regenerate Kilo HttpApi branding.

---------

Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Michael Hart <mhart@cloudflare.com>
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Co-authored-by: Simon Klee <hello@simonklee.dk>
Co-authored-by: smagnuso <smagnuso@gmail.com>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
Co-authored-by: Orca丶 <93272799+dauphinYan@users.noreply.github.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
Co-authored-by: Ulises Jeremias <ulisescf.24@gmail.com>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: Jérôme Benoit <jerome.benoit@sap.com>
Co-authored-by: Ariane Emory <97994360+ariane-emory@users.noreply.github.com>
Co-authored-by: LIU Xinyu <contact@lxy.cc>
Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
Co-authored-by: Sebastian <hasta84@gmail.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Kamesh Sampath <kamesh.sampath@hotmail.com>
Co-authored-by: Cortex Code <noreply@snowflake.com>
Co-authored-by: pcadena-lila <pcadena@lila.ai>
Co-authored-by: weiconghe <46336277+weiconghe@users.noreply.github.com>
Co-authored-by: alberto <914199+alblez@users.noreply.github.com>
Co-authored-by: kilo-maintainer[bot] <kilo-maintainer[bot]@users.noreply.github.com>
2026-07-13 18:00:35 +02:00

1046 lines
39 KiB
TypeScript

import { afterEach, describe, expect } from "bun:test"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Deferred, Effect, Layer } from "effect"
import type * as Scope from "effect/Scope"
import { HttpServer } from "effect/unstable/http"
import { ChildProcessSpawner } from "effect/unstable/process"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Flag } from "@opencode-ai/core/flag/flag"
import { createKiloClient } from "@kilocode/sdk/v2"
import { validateSession } from "../../src/cli/cmd/tui/validate-session"
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
import { InstanceStore } from "../../src/project/instance-store"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { MessageV2 } from "../../src/session/message-v2"
import type { Config } from "@/config/config"
import { Session as SessionNs } from "@/session/session"
import { errorMessage } from "../../src/util/error"
import { TestLLMServer } from "../lib/llm-server"
import path from "path"
import { resetDatabase } from "../fixture/db"
import { disposeAllInstances, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { awaitWithTimeout, pollWithTimeout, testEffect } from "../lib/effect" // kilocode_change
import { testProviderConfig } from "../lib/test-provider"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Database } from "@opencode-ai/core/database/database"
import { httpApiLayer } from "./httpapi-layer"
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
const it = testEffect(
Layer.mergeAll(
FSUtil.defaultLayer,
CrossSpawnSpawner.defaultLayer,
InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)),
Database.defaultLayer,
httpApiLayer,
),
)
const original = {
KILO_SERVER_PASSWORD: Flag.KILO_SERVER_PASSWORD,
KILO_SERVER_USERNAME: Flag.KILO_SERVER_USERNAME,
}
type ServerPath = "default" | "raw"
type Sdk = ReturnType<typeof createKiloClient>
type SdkResult = { response: Response; data?: unknown; error?: unknown }
type Captured = { status: number; data?: unknown; error?: unknown }
type ProjectFixture = { sdk: Sdk; directory: string }
type LlmProjectFixture = ProjectFixture & { llm: TestLLMServer["Service"] }
type TestServices =
| FSUtil.Service
| ChildProcessSpawner.ChildProcessSpawner
| InstanceStore.Service
| HttpServer.HttpServer
type TestScope = Scope.Scope | TestServices
function client(
serverPath: ServerPath,
directory?: string,
input?: {
password?: string
username?: string
headers?: Record<string, string>
workspaceID?: string
onRequest?: (request: Request) => void
},
) {
return serverFetch(serverPath, input).pipe(
Effect.map((fetch) =>
createKiloClient({
baseUrl: "http://localhost",
directory,
experimental_workspaceID: input?.workspaceID,
headers: input?.headers,
fetch,
}),
),
)
}
function serverFetch(
serverPath: ServerPath,
input?: { password?: string; username?: string; onRequest?: (request: Request) => void },
) {
return HttpServer.HttpServer.use((server) =>
Effect.sync(() => {
void serverPath
Flag.KILO_SERVER_PASSWORD = input?.password
Flag.KILO_SERVER_USERNAME = input?.username
const baseUrl = HttpServer.formatAddress(server.address)
return Object.assign(
async (request: RequestInfo | URL, init?: RequestInit) => {
const source = request instanceof Request ? request : new Request(request, init)
input?.onRequest?.(source)
const url = new URL(source.url)
return globalThis.fetch(new Request(new URL(`${url.pathname}${url.search}`, baseUrl), source))
},
{ preconnect: globalThis.fetch.preconnect },
) satisfies typeof globalThis.fetch
}),
)
}
function authorization(username: string, password: string) {
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
}
function call<T>(request: () => Promise<T>) {
return Effect.promise(request)
}
function capture(request: () => Promise<SdkResult>) {
return call(request).pipe(
Effect.map((result) => ({
status: result.response.status,
data: result.data,
error: result.error,
})),
)
}
function captureThrown(request: () => Promise<unknown>) {
return call(async () => {
try {
await request()
} catch (error) {
return error
}
})
}
function expectStatus(request: () => Promise<{ response: Response }>, status: number) {
return call(request).pipe(
Effect.tap((result) => Effect.sync(() => expect(result.response.status).toBe(status))),
Effect.asVoid,
)
}
function firstEvent(open: (signal: AbortSignal) => Promise<{ stream: AsyncIterator<unknown> }>) {
return Effect.acquireRelease(
Effect.sync(() => new AbortController()),
(controller) => Effect.sync(() => controller.abort()),
).pipe(
Effect.flatMap((controller) =>
Effect.acquireRelease(
call(() => open(controller.signal)),
(events) => call(async () => void (await events.stream.return?.(undefined))).pipe(Effect.ignore),
).pipe(
Effect.flatMap((events) =>
call(() => events.stream.next()).pipe(
Effect.timeoutOrElse({
duration: "1 second",
orElse: () => Effect.fail(new Error("timed out waiting for SDK event")),
}),
),
),
Effect.map((result) => result.value),
),
),
)
}
function record(value: unknown) {
return value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : {}
}
function array(value: unknown) {
return Array.isArray(value) ? value : []
}
function statuses(input: Record<string, Captured>) {
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, value.status]))
}
function firstPartText(value: unknown) {
return record(array(record(value).parts)[0]).text
}
// kilocode_change start
function texts(value: unknown) {
return array(value)
.flatMap((item) => array(record(item).parts))
.map((part) => record(part).text)
.filter((text): text is string => typeof text === "string")
}
// kilocode_change end
function sessionTitles(value: unknown) {
return array(value)
.map((item) => record(item).title)
.filter((title): title is string => typeof title === "string")
.sort()
}
function resetState() {
return Effect.promise(async () => {
await disposeAllInstances()
await resetDatabase()
})
}
function httpapi<A, E>(name: string, effect: Effect.Effect<A, E, TestScope>) {
it.live(name, effect)
}
function httpapiInstance<A, E>(
name: string,
options: {
serverPath: ServerPath
git?: boolean
config?: Partial<ConfigV1.Info>
setup?: (dir: string) => Effect.Effect<void, E, TestServices>
},
run: (input: ProjectFixture) => Effect.Effect<A, E, TestScope>,
) {
it.instance(
name,
Effect.gen(function* () {
const instance = yield* TestInstance
yield* options.setup?.(instance.directory) ?? Effect.void
return yield* run({ sdk: yield* client(options.serverPath, instance.directory), directory: instance.directory })
}),
{ git: options.git ?? true, config: { formatter: false, lsp: false, ...options.config } },
)
}
function serverPathParity<A, E>(name: string, scenario: (serverPath: ServerPath) => Effect.Effect<A, E, TestScope>) {
it.live(name, scenario("raw"))
}
function withProject<A, E, E2 = never>(
serverPath: ServerPath,
options: {
git?: boolean
config?: Partial<ConfigV1.Info>
setup?: (dir: string) => Effect.Effect<void, E2, TestServices>
},
run: (input: ProjectFixture) => Effect.Effect<A, E, TestScope>,
) {
return Effect.gen(function* () {
const directory = yield* tmpdirScoped({
git: options.git ?? false,
config: { formatter: false, lsp: false, ...options.config },
})
yield* options.setup?.(directory) ?? Effect.void
return yield* run({ sdk: yield* client(serverPath, directory), directory })
})
}
function withStandardProject<A, E>(
serverPath: ServerPath,
run: (input: ProjectFixture) => Effect.Effect<A, E, TestScope>,
) {
return withProject(serverPath, { setup: writeStandardFiles }, run)
}
function withFakeLlm<A, E>(serverPath: ServerPath, run: (input: LlmProjectFixture) => Effect.Effect<A, E, TestScope>) {
return Effect.gen(function* () {
const llm = yield* TestLLMServer
return yield* withProject(serverPath, { config: testProviderConfig(llm.url) }, (input) => run({ ...input, llm }))
}).pipe(Effect.provide(TestLLMServer.layer))
}
function withFakeLlmProject<A, E>(
serverPath: ServerPath,
options: { setup?: (dir: string) => Effect.Effect<void, E, TestServices> },
run: (input: LlmProjectFixture) => Effect.Effect<A, E, TestScope>,
) {
return Effect.gen(function* () {
const llm = yield* TestLLMServer
return yield* withProject(
serverPath,
{
config: testProviderConfig(llm.url),
setup: options.setup,
},
(input) => run({ ...input, llm }),
)
}).pipe(Effect.provide(TestLLMServer.layer))
}
function writeStandardFiles(dir: string) {
return FSUtil.Service.use((fs) =>
Effect.all([
fs.writeWithDirs(path.join(dir, "hello.txt"), "hello"),
fs.writeWithDirs(path.join(dir, "needle.ts"), "export const needle = 'sdk-parity'\n"),
]).pipe(Effect.asVoid),
)
}
function writeProjectSkill(dir: string) {
return FSUtil.Service.use((fs) =>
fs.writeWithDirs(
path.join(dir, ".kilo", "skills", "project-rest-skill", "SKILL.md"), // kilocode_change
`---
name: project-rest-skill
description: A project skill visible to REST API prompts.
---
# Project REST Skill
`,
),
)
}
function seedMessage(directory: string, sessionID: string) {
const id = SessionID.make(sessionID)
return InstanceStore.Service.use((store) =>
store.provide(
{ directory },
SessionNs.Service.use((svc) =>
Effect.gen(function* () {
const message = yield* svc.updateMessage({
id: MessageID.ascending(),
sessionID: id,
role: "user",
time: { created: Date.now() },
agent: "test",
model: { providerID: ProviderV2.ID.make("test"), modelID: ModelV2.ID.make("test") },
tools: {},
} satisfies SessionV1.User)
const part = yield* svc.updatePart({
id: PartID.ascending(),
sessionID: id,
messageID: message.id,
type: "text",
text: "seeded message",
})
return { message, part }
}),
).pipe(Effect.provide(SessionNs.defaultLayer)),
),
)
}
afterEach(async () => {
Flag.KILO_SERVER_PASSWORD = original.KILO_SERVER_PASSWORD
Flag.KILO_SERVER_USERNAME = original.KILO_SERVER_USERNAME
await disposeAllInstances()
await resetDatabase()
})
describe("HttpApi SDK", () => {
httpapi(
"uses the generated SDK for global and control routes",
Effect.gen(function* () {
const sdk = yield* client("raw")
const health = yield* call(() => sdk.global.health())
const log = yield* call(() => sdk.app.log({ service: "httpapi-sdk-test", level: "info", message: "hello" }))
expect(health.response.status).toBe(200)
expect(health.data).toMatchObject({ healthy: true })
expect(yield* firstEvent((signal) => sdk.global.event({ signal }))).toMatchObject({
payload: { type: "server.connected" },
})
expect(log.response.status).toBe(200)
expect(log.data).toBe(true)
yield* expectStatus(() => sdk.auth.set({ providerID: "test" }), 400)
}),
)
httpapiInstance(
"uses the generated SDK for safe instance routes",
{ serverPath: "raw", git: false, setup: writeStandardFiles },
({ sdk }) =>
Effect.gen(function* () {
const file = yield* call(() => sdk.file.read({ path: "hello.txt" }))
const session = yield* call(() => sdk.session.create({ title: "sdk" }))
const listed = yield* call(() => sdk.session.list({ roots: true, limit: 10 }))
expect(file.response.status).toBe(200)
expect(file.data).toMatchObject({ content: "hello" })
expect(session.response.status).toBe(200)
expect(session.data).toMatchObject({ title: "sdk" })
expect(listed.response.status).toBe(200)
expect(listed.data?.map((item) => item.id)).toContain(session.data?.id)
yield* Effect.all([
expectStatus(() => sdk.project.current(), 200),
expectStatus(() => sdk.config.get(), 200),
expectStatus(() => sdk.config.providers(), 200),
expectStatus(() => sdk.find.files({ query: "hello", limit: 10 }), 200),
])
}),
)
httpapi(
"routes configured SDK directory and workspace for v2 location GETs",
withProject("raw", { setup: writeStandardFiles }, ({ directory }) =>
Effect.gen(function* () {
const workspaceID = "wrk_sdk"
let request: Request | undefined
const sdk = yield* client("raw", directory, {
workspaceID,
onRequest: (value) => (request = value),
})
const file = yield* call(() => sdk.v2.fs.read({ path: "hello.txt" }))
const url = new URL(request!.url)
expect(file.response.status).toBe(200)
expect(file.data).toMatchObject({ data: { content: "hello" } })
expect(url.searchParams.get("directory")).toBe(directory)
expect(url.searchParams.get("workspace")).toBe(workspaceID)
expect(url.searchParams.get("location[directory]")).toBe(directory)
expect(url.searchParams.get("location[workspace]")).toBe(workspaceID)
expect(request!.headers.has("x-kilo-directory")).toBe(false)
expect(request!.headers.has("x-kilo-workspace")).toBe(false)
}),
),
)
serverPathParity("matches generated SDK global and control behavior", (serverPath) =>
Effect.gen(function* () {
const sdk = yield* client(serverPath)
const health = yield* capture(() => sdk.global.health())
const log = yield* capture(() => sdk.app.log({ service: "sdk-parity", level: "info", message: "hello" }))
const invalidAuth = yield* capture(() => sdk.auth.set({ providerID: "test" }))
return {
statuses: statuses({ health, log, invalidAuth }),
health: record(health.data).healthy,
log: log.data,
}
}),
)
serverPathParity("matches generated SDK global event stream", (serverPath) =>
Effect.gen(function* () {
const sdk = yield* client(serverPath)
const event = yield* firstEvent((signal) => sdk.global.event({ signal }))
return { type: record(record(event).payload).type }
}),
)
serverPathParity("matches generated SDK instance event stream", (serverPath) =>
withStandardProject(serverPath, ({ sdk }) =>
firstEvent((signal) => sdk.event.subscribe(undefined, { signal })).pipe(
Effect.map((event) => ({ type: record(record(event).payload).type })),
),
),
)
serverPathParity("matches generated SDK missing session errors", (serverPath) =>
withStandardProject(serverPath, ({ sdk }) =>
Effect.gen(function* () {
const sessionID = "ses_missing"
const expected = {
name: "NotFoundError",
data: { message: `Session not found: ${sessionID}` },
}
const missing = yield* capture(() => sdk.session.get({ sessionID }))
const thrown = yield* captureThrown(() => sdk.session.get({ sessionID }, { throwOnError: true }))
// Result-tuple path: error body is preserved as-is so existing
// consumers reading `result.error.name` / `JSON.stringify(error)`
// keep working byte-for-byte.
expect(missing.error).toEqual(expected)
// throwOnError path: SDK wraps the body in a real Error with the
// server's message, with the original parsed body preserved under
// `.cause.body`.
expect(thrown).toBeInstanceOf(Error)
expect((thrown as Error).message).toBe(expected.data.message)
expect(((thrown as Error).cause as { body: unknown }).body).toEqual(expected)
return {
status: missing.status,
error: missing.error,
thrown,
}
}),
),
)
serverPathParity("formats missing session validation errors for -s", (serverPath) =>
withStandardProject(serverPath, ({ directory }) =>
Effect.gen(function* () {
const sessionID = "ses_206f84f18ffeZ6hhD7pFYAiW5T"
const fetch = yield* serverFetch(serverPath)
const thrown = yield* captureThrown(() =>
validateSession({
url: "http://localhost",
directory,
sessionID,
fetch,
}),
)
expect(errorMessage(thrown)).toBe(`Session not found: ${sessionID}`)
return errorMessage(thrown)
}),
),
)
httpapiInstance(
"uses generated SDK basic auth behavior",
{ serverPath: "raw", setup: writeStandardFiles },
({ directory }) =>
Effect.gen(function* () {
const missingSdk = yield* client("raw", directory, { password: "secret" })
const missing = yield* capture(() => missingSdk.file.read({ path: "hello.txt" }))
// kilocode_change start - match Hono AuthMiddleware username default ("kilo")
const badSdk = yield* client("raw", directory, {
password: "secret",
headers: { authorization: authorization("kilo", "wrong") },
})
const bad = yield* capture(() => badSdk.file.read({ path: "hello.txt" }))
const goodSdk = yield* client("raw", directory, {
password: "secret",
headers: { authorization: authorization("kilo", "secret") },
})
const good = yield* capture(() => goodSdk.file.read({ path: "hello.txt" }))
// kilocode_change end
return {
statuses: statuses({ missing, bad, good }),
content: record(good.data).content,
}
}),
)
serverPathParity("matches generated SDK instance read routes", (serverPath) =>
withProject(serverPath, { git: true, setup: writeStandardFiles }, ({ sdk, directory }) =>
Effect.gen(function* () {
const project = yield* capture(() => sdk.project.current())
const projects = yield* capture(() => sdk.project.list())
const paths = yield* capture(() => sdk.path.get())
const config = yield* capture(() => sdk.config.get())
const providers = yield* capture(() => sdk.config.providers())
const file = yield* capture(() => sdk.file.read({ path: "hello.txt" }))
const files = yield* capture(() => sdk.file.list({ path: "." }))
const fileStatus = yield* capture(() => sdk.file.status())
const findFiles = yield* capture(() => sdk.find.files({ query: "hello", limit: 10 }))
const findText = yield* capture(() => sdk.find.text({ pattern: "sdk-parity" }))
const agents = yield* capture(() => sdk.app.agents())
const skills = yield* capture(() => sdk.app.skills())
const tools = yield* capture(() => sdk.tool.ids())
const vcs = yield* capture(() => sdk.vcs.get())
const formatter = yield* capture(() => sdk.formatter.status())
const lsp = yield* capture(() => sdk.lsp.status())
return {
statuses: statuses({
project,
projects,
paths,
config,
providers,
file,
files,
fileStatus,
findFiles,
findText,
agents,
skills,
tools,
vcs,
formatter,
lsp,
}),
project: { worktreeSelected: record(project.data).worktree === directory },
paths: { directorySelected: record(paths.data).directory === directory },
file: record(file.data).content,
hasProject: array(projects.data).length > 0,
foundFile: JSON.stringify(findFiles.data).includes("hello.txt"),
foundText: JSON.stringify(findText.data ?? null).includes("sdk-parity"),
listedFile: JSON.stringify(files.data).includes("hello.txt"),
vcs: { hasBranch: typeof record(vcs.data).branch === "string" },
}
}),
),
)
serverPathParity("matches generated SDK session lifecycle routes", (serverPath) =>
withStandardProject(serverPath, ({ sdk }) =>
Effect.gen(function* () {
const parent = yield* capture(() => sdk.session.create({ title: "parent" }))
const parentID = String(record(parent.data).id)
const child = yield* capture(() => sdk.session.create({ title: "child", parentID }))
const childID = String(record(child.data).id)
const get = yield* capture(() => sdk.session.get({ sessionID: parentID }))
const update = yield* capture(() => sdk.session.update({ sessionID: parentID, title: "renamed" }))
const roots = yield* capture(() => sdk.session.list({ roots: true, limit: 10 }))
const all = yield* capture(() => sdk.session.list({ roots: false, limit: 10 }))
const children = yield* capture(() => sdk.session.children({ sessionID: parentID }))
const todo = yield* capture(() => sdk.session.todo({ sessionID: parentID }))
const status = yield* capture(() => sdk.session.status())
const messages = yield* capture(() => sdk.session.messages({ sessionID: parentID }))
const missingGet = yield* capture(() => sdk.session.get({ sessionID: "ses_missing" }))
const missingMessages = yield* capture(() => sdk.session.messages({ sessionID: "ses_missing", limit: 2 }))
const invalidCursor = yield* capture(() =>
sdk.session.messages({ sessionID: parentID, limit: 2, before: "bad" }),
)
const deleted = yield* capture(() => sdk.session.delete({ sessionID: childID }))
const getDeleted = yield* capture(() => sdk.session.get({ sessionID: childID }))
return {
statuses: statuses({
parent,
child,
get,
update,
roots,
all,
children,
todo,
status,
messages,
missingGet,
missingMessages,
invalidCursor,
deleted,
getDeleted,
}),
getTitle: record(get.data).title,
updatedTitle: record(update.data).title,
rootTitles: sessionTitles(roots.data),
allTitles: sessionTitles(all.data),
childCount: array(children.data).length,
todoCount: array(todo.data).length,
messageCount: array(messages.data).length,
}
}),
),
)
serverPathParity("matches generated SDK session message and part routes", (serverPath) =>
withStandardProject(serverPath, ({ sdk, directory }) =>
Effect.gen(function* () {
const session = yield* capture(() => sdk.session.create({ title: "messages" }))
const sessionID = String(record(session.data).id)
const seeded = yield* seedMessage(directory, sessionID)
const list = yield* capture(() => sdk.session.messages({ sessionID }))
const page = yield* capture(() => sdk.session.messages({ sessionID, limit: 1 }))
const message = yield* capture(() => sdk.session.message({ sessionID, messageID: seeded.message.id }))
const partUpdate = yield* capture(() =>
sdk.part.update({
sessionID,
messageID: seeded.message.id,
partID: seeded.part.id,
part: { ...seeded.part, text: "updated message" } as NonNullable<
Parameters<Sdk["part"]["update"]>[0]["part"]
>,
}),
)
const updated = yield* capture(() => sdk.session.message({ sessionID, messageID: seeded.message.id }))
const partDelete = yield* capture(() =>
sdk.part.delete({ sessionID, messageID: seeded.message.id, partID: seeded.part.id }),
)
const withoutPart = yield* capture(() => sdk.session.message({ sessionID, messageID: seeded.message.id }))
const deleteMessage = yield* capture(() =>
sdk.session.deleteMessage({ sessionID, messageID: seeded.message.id }),
)
const missingMessage = yield* capture(() => sdk.session.message({ sessionID, messageID: seeded.message.id }))
return {
statuses: statuses({
session,
list,
page,
message,
partUpdate,
updated,
partDelete,
withoutPart,
deleteMessage,
missingMessage,
}),
listCount: array(list.data).length,
pageCount: array(page.data).length,
initialText: firstPartText(message.data),
updatedText: firstPartText(updated.data),
partCountAfterDelete: array(record(withoutPart.data).parts).length,
}
}),
),
)
// Regression: EventV2 must publish on the same ProjectBus the /event handler
// subscribes to, AND the /event stream must forward handler ALS/context into the
// body-pump fiber. Drives the full SDK → /event → Session.updatePart → sync.run →
// bus.publish → SDK subscriber path. Goes red if either the publisher uses a
// different bus instance (Bug 2 / pre-#27825) or the stream loses context (Bug 1 /
// pre-#27425).
serverPathParity("streams sync-backed part updates to /event subscribers", (serverPath) =>
withStandardProject(serverPath, ({ sdk, directory }) =>
Effect.gen(function* () {
const session = yield* capture(() => sdk.session.create({ title: "sync-backed part event" }))
const sessionID = String(record(session.data).id)
const seeded = yield* seedMessage(directory, sessionID)
const controller = new AbortController()
yield* Effect.addFinalizer(() => Effect.sync(() => controller.abort()))
const events = yield* call(() => sdk.event.subscribe(undefined, { signal: controller.signal }))
yield* Effect.addFinalizer(() =>
call(async () => void (await events.stream.return?.(undefined))).pipe(Effect.ignore),
)
const ready = yield* Deferred.make<void>()
const received = yield* Deferred.make<unknown>()
yield* call(async () => {
for await (const event of events.stream) {
const payload = record(event).payload ?? event
const type = record(payload).type
if (type === "server.connected") {
Deferred.doneUnsafe(ready, Effect.void)
continue
}
if (type === MessageV2.Event.PartUpdated.type) {
Deferred.doneUnsafe(received, Effect.succeed(payload))
return
}
}
}).pipe(Effect.forkScoped)
yield* awaitWithTimeout(Deferred.await(ready), "timed out waiting for /event server.connected", "2 seconds")
const updated = yield* capture(() =>
sdk.part.update({
sessionID,
messageID: seeded.message.id,
partID: seeded.part.id,
part: { ...seeded.part, text: "updated via sync" } as NonNullable<
Parameters<Sdk["part"]["update"]>[0]["part"]
>,
}),
)
expect(updated.status).toBe(200)
const event = yield* awaitWithTimeout(
Deferred.await(received),
"timed out waiting for message.part.updated bus payload over /event",
"5 seconds",
)
const properties = record(record(event).properties)
expect(record(properties.part)).toMatchObject({ id: seeded.part.id, type: "text" })
return { type: record(event).type, partType: record(properties.part).type }
}),
),
)
serverPathParity("matches generated SDK prompt no-reply routes", (serverPath) =>
withStandardProject(serverPath, ({ sdk }) =>
Effect.gen(function* () {
const session = yield* capture(() => sdk.session.create({ title: "prompt" }))
const sessionID = String(record(session.data).id)
const prompt = yield* capture(() =>
sdk.session.prompt({
sessionID,
agent: "build",
noReply: true,
parts: [{ type: "text", text: "hello" }],
}),
)
// kilocode_change start
const asyncPrompt = yield* capture(() =>
sdk.session.promptAsync({
sessionID,
agent: "build",
noReply: true,
parts: [{ type: "text", text: "async hello" }],
}),
)
const messages = yield* pollWithTimeout(
capture(() => sdk.session.messages({ sessionID })).pipe(
Effect.map((messages) => (texts(messages.data).includes("async hello") ? messages : undefined)),
),
"async no-reply prompt message was not persisted",
)
return {
statuses: statuses({ session, prompt, asyncPrompt, messages }),
promptRole: record(record(prompt.data).info).role,
messageCount: array(messages.data).length,
messageTexts: texts(messages.data).sort(),
}
// kilocode_change end
}),
),
)
// kilocode_change start - verify invalid user images fail at the real SDK boundary
serverPathParity("rejects malformed user image data before persistence", (serverPath) =>
withStandardProject(serverPath, ({ sdk }) =>
Effect.gen(function* () {
const session = yield* capture(() => sdk.session.create({ title: "invalid image" }))
const sessionID = String(record(session.data).id)
const prompt = yield* capture(() =>
sdk.session.prompt({
sessionID,
agent: "build",
noReply: true,
parts: [
{
type: "file",
mime: "image/png",
filename: "not-an-image.png",
url: "data:image/png;base64,bm90LWltYWdl",
},
],
}),
)
const messages = yield* capture(() => sdk.session.messages({ sessionID }))
expect(prompt.status).toBe(400)
expect(JSON.stringify(messages.data)).not.toContain("not-an-image.png")
return {
promptStatus: prompt.status,
persisted: JSON.stringify(messages.data).includes("not-an-image.png"),
}
}),
),
)
serverPathParity("rejects oversized user image files before persistence", (serverPath) =>
withProject(serverPath, { config: { attachment: { image: { max_base64_bytes: 4 } } } }, ({ sdk, directory }) =>
Effect.gen(function* () {
const filepath = path.join(directory, "oversized.png")
yield* call(() => Bun.write(filepath, Buffer.alloc(1024, 1)))
const session = yield* capture(() => sdk.session.create({ title: "oversized image" }))
const sessionID = String(record(session.data).id)
const prompt = yield* capture(() =>
sdk.session.prompt({
sessionID,
agent: "build",
noReply: true,
parts: [
{
type: "file",
mime: "image/png",
filename: "oversized.png",
url: `file://${filepath}`,
},
],
}),
)
const messages = yield* capture(() => sdk.session.messages({ sessionID }))
expect(prompt.status).toBe(400)
expect(JSON.stringify(messages.data)).not.toContain("oversized.png")
return { promptStatus: prompt.status, persisted: JSON.stringify(messages.data).includes("oversized.png") }
}),
),
)
// kilocode_change end
serverPathParity("matches generated SDK prompt streaming through fake LLM", (serverPath) =>
withFakeLlm(serverPath, ({ sdk, llm }) =>
Effect.gen(function* () {
yield* llm.text("fake world", { usage: { input: 11, output: 7 } })
const session = yield* capture(() =>
sdk.session.create({
title: "llm prompt",
permission: [{ permission: "*", pattern: "*", action: "allow" }],
}),
)
const sessionID = String(record(session.data).id)
const prompt = yield* capture(() =>
sdk.session.prompt({
sessionID,
agent: "build",
model: { providerID: "test", modelID: "test-model" },
parts: [{ type: "text", text: "hello llm" }],
}),
)
const messages = yield* capture(() => sdk.session.messages({ sessionID }))
const inputs = yield* llm.inputs
return {
statuses: statuses({ session, prompt, messages }),
calls: inputs.length,
requestedModel: inputs[0]?.model,
responseText: JSON.stringify(prompt.data).includes("fake world"),
persistedText: JSON.stringify(messages.data).includes("fake world"),
userText: JSON.stringify(messages.data).includes("hello llm"),
}
}),
),
)
// kilocode_change start - verify provider errors remain in successful assistant messages
serverPathParity("preserves provider errors through the generated SDK", (serverPath) =>
withFakeLlm(serverPath, ({ sdk, llm }) =>
Effect.gen(function* () {
const gateway = { error: { code: "PAID_MODEL_AUTH_REQUIRED", message: "Authentication required" } }
const create = () =>
capture(() =>
sdk.session.create({
title: "provider error",
permission: [{ permission: "*", pattern: "*", action: "allow" }],
}),
)
const prompt = (sessionID: string) => ({
sessionID,
agent: "build",
model: { providerID: "test", modelID: "test-model" },
parts: [{ type: "text" as const, text: "trigger provider error" }],
})
yield* llm.error(401, gateway)
const tupleSession = yield* create()
const tuple = yield* capture(() => sdk.session.prompt(prompt(String(record(tupleSession.data).id))))
yield* llm.error(401, gateway)
const strictSession = yield* create()
const strict = yield* call(() =>
sdk.session.prompt(prompt(String(record(strictSession.data).id)), { throwOnError: true }),
)
const tupleError = record(record(tuple.data).info).error
const tupleData = record(record(tupleError).data)
const strictError = record(record(record(strict).data).info).error
const strictData = record(record(strictError).data)
expect(tuple.status).toBe(200)
expect(record(tupleError).name).toBe("APIError")
expect(tupleData.statusCode).toBe(401)
expect(JSON.parse(String(tupleData.responseBody))).toEqual(gateway)
expect(record(strictError).name).toBe("APIError")
expect(strictData.statusCode).toBe(401)
expect(JSON.parse(String(strictData.responseBody))).toEqual(gateway)
return {
tupleStatus: tuple.status,
tupleName: record(tupleError).name,
providerStatus: tupleData.statusCode,
providerBody: tupleData.responseBody,
strictName: record(strictError).name,
strictStatus: strictData.statusCode,
strictBody: strictData.responseBody,
}
}),
),
)
// kilocode_change end
httpapi(
"includes project skills in REST API prompt context",
withFakeLlmProject("default", { setup: writeProjectSkill }, ({ sdk, llm }) =>
Effect.gen(function* () {
yield* llm.text("skill context ok", { usage: { input: 11, output: 7 } })
const session = yield* capture(() =>
sdk.session.create({
title: "project skill prompt",
permission: [{ permission: "*", pattern: "*", action: "allow" }],
}),
)
const sessionID = String(record(session.data).id)
const prompt = yield* capture(() =>
sdk.session.prompt({
sessionID,
agent: "build",
model: { providerID: "test", modelID: "test-model" },
parts: [{ type: "text", text: "hello skill context" }],
}),
)
const inputs = yield* llm.inputs
expect(session.status).toBe(200)
expect(prompt.status).toBe(200)
expect(JSON.stringify(inputs[0])).toContain("project-rest-skill")
}),
),
)
serverPathParity("matches generated SDK TUI validation and command routes", (serverPath) =>
withStandardProject(serverPath, ({ sdk }) =>
Effect.gen(function* () {
const session = yield* capture(() => sdk.session.create({ title: "tui" }))
const sessionID = String(record(session.data).id)
const appendPrompt = yield* capture(() => sdk.tui.appendPrompt({ text: "hello" }))
const openHelp = yield* capture(() => sdk.tui.openHelp())
const openSessions = yield* capture(() => sdk.tui.openSessions())
const openThemes = yield* capture(() => sdk.tui.openThemes())
const openModels = yield* capture(() => sdk.tui.openModels())
const submitPrompt = yield* capture(() => sdk.tui.submitPrompt())
const clearPrompt = yield* capture(() => sdk.tui.clearPrompt())
const executeCommand = yield* capture(() => sdk.tui.executeCommand({ command: "session_new" }))
const showToast = yield* capture(() => sdk.tui.showToast({ title: "SDK", message: "hello", variant: "info" }))
const selectSession = yield* capture(() => sdk.tui.selectSession({ sessionID }))
const missingSession = yield* capture(() => sdk.tui.selectSession({ sessionID: "ses_missing" }))
const invalidSession = yield* capture(() => sdk.tui.selectSession({ sessionID: "invalid_session_id" }))
return {
statuses: statuses({
session,
appendPrompt,
openHelp,
openSessions,
openThemes,
openModels,
submitPrompt,
clearPrompt,
executeCommand,
showToast,
selectSession,
missingSession,
invalidSession,
}),
data: {
appendPrompt: appendPrompt.data,
openHelp: openHelp.data,
openSessions: openSessions.data,
openThemes: openThemes.data,
openModels: openModels.data,
submitPrompt: submitPrompt.data,
clearPrompt: clearPrompt.data,
executeCommand: executeCommand.data,
showToast: showToast.data,
selectSession: selectSession.data,
},
}
}),
),
)
serverPathParity("matches generated SDK project git initialization", (serverPath) =>
withProject(serverPath, {}, ({ sdk, directory }) =>
Effect.gen(function* () {
const before = yield* capture(() => sdk.project.current())
const init = yield* capture(() => sdk.project.initGit())
const after = yield* capture(() => sdk.project.current())
return {
statuses: statuses({ before, init, after }),
before: {
vcs: record(before.data).vcs ?? null,
worktree: record(before.data).worktree,
},
init: {
vcs: record(init.data).vcs,
worktreeSelected: record(init.data).worktree === directory,
},
after: {
vcs: record(after.data).vcs,
worktreeSelected: record(after.data).worktree === directory,
},
}
}),
),
)
})