opencode/packages/core/test/session-runner-recorded.test.ts
Kit Langton cfdb9ab705 refactor(core): delete the system context registry
Builtins and instruction context become ordinary Location-scoped
services exposing load(), the same shape as skill, reference, and MCP
guidance. The runner composes all six producers explicitly in
loadSystemContext, so the full context roster is readable at one call
site. Dynamic registration and scoped deregistration had no consumers.
2026-07-02 21:21:11 -04:00

205 lines
8.6 KiB
TypeScript

import { HttpRecorder } from "@opencode-ai/http-recorder"
import { HttpRecorderInternal } from "@opencode-ai/http-recorder/internal"
import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
import { Auth, LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { EventTable } from "@opencode-ai/core/event/sql"
import { Job } from "@opencode-ai/core/job"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Config } from "@opencode-ai/core/config"
import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { Snapshot } from "@opencode-ai/core/snapshot"
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
import { SessionTitle } from "@opencode-ai/core/session/title"
import { Prompt } from "@opencode-ai/core/session/prompt"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionExecution } from "@opencode-ai/core/session/execution"
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
import { SessionRunner } from "@opencode-ai/core/session/runner"
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { SessionStore } from "@opencode-ai/core/session/store"
import { Location } from "@opencode-ai/core/location"
import { SystemContextBuiltIns } from "@opencode-ai/core/system-context/builtins"
import { InstructionContext } from "@opencode-ai/core/instruction-context"
import { SystemContext } from "@opencode-ai/core/system-context"
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
import { McpGuidance } from "@opencode-ai/core/mcp/guidance"
import { describe, expect } from "bun:test"
import { eq } from "drizzle-orm"
import { Effect, Layer } from "effect"
import path from "node:path"
import { testEffect } from "./lib/effect"
const cassette =
process.env.RECORD === "true"
? HttpRecorderInternal.cassetteLayer("session-runner/openai-chat-streams-text", {
directory: path.resolve(import.meta.dir, "fixtures/recordings"),
mode: "record",
})
: HttpRecorder.http("session-runner/openai-chat-streams-text", {
directory: path.resolve(import.meta.dir, "fixtures/recordings"),
})
const executor = RequestExecutor.layer.pipe(Layer.provide(cassette))
const client = LLMClient.layer.pipe(Layer.provide(executor))
const permission = Layer.succeed(
PermissionV2.Service,
PermissionV2.Service.of({
assert: () => Effect.die("unused"),
ask: () => Effect.die("unused"),
reply: () => Effect.die("unused"),
get: () => Effect.die("unused"),
forSession: () => Effect.die("unused"),
list: () => Effect.die("unused"),
}),
)
const model = OpenAIChat.route
.with({
endpoint: { baseURL: "https://api.openai.com/v1" },
auth: Auth.bearer(process.env.OPENAI_API_KEY ?? "fixture"),
generation: { maxTokens: 20, temperature: 0 },
})
.model({ id: "gpt-4o-mini" })
const models = SessionRunnerModel.layerWith(() => Effect.succeed(model))
const systemContext = Layer.mock(SystemContextBuiltIns.Service, { load: () => Effect.succeed(SystemContext.empty) })
const instructionContext = Layer.mock(InstructionContext.Service, { load: () => Effect.succeed(SystemContext.empty) })
const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
[Snapshot.node, Snapshot.noopLayer],
[LayerNodePlatform.llmClient, client],
[SessionRunnerModel.node, models],
[SystemContextBuiltIns.node, systemContext],
[InstructionContext.node, instructionContext],
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
[SkillGuidance.node, skillGuidance],
[ReferenceGuidance.node, referenceGuidance],
[McpGuidance.node, mcpGuidance],
[Config.node, config],
[PermissionV2.node, permission],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
])
const execution = Layer.effect(
SessionExecution.Service,
Effect.gen(function* () {
const sessionRunner = yield* SessionRunner.Service
const coordinator = yield* SessionRunCoordinator.make<SessionV2.ID, SessionRunner.RunError>({
drain: (sessionID, force) => sessionRunner.run({ sessionID, force }),
})
return SessionExecution.Service.of({
active: coordinator.active,
resume: coordinator.run,
wake: coordinator.wake,
interrupt: coordinator.interrupt,
awaitIdle: coordinator.awaitIdle,
})
}),
).pipe(Layer.provide(runnerLayer))
const it = testEffect(
AppNodeBuilder.build(
LayerNode.group([
Database.node,
EventV2.node,
SessionProjector.node,
SessionStore.node,
AgentV2.node,
ToolRegistry.node,
SessionRunnerModel.node,
SystemContextBuiltIns.node,
InstructionContext.node,
SkillGuidance.node,
ReferenceGuidance.node,
Config.node,
Snapshot.node,
SessionRunnerLLM.node,
SessionV2.node,
]),
[
[LayerNodePlatform.llmClient, client],
[PermissionV2.node, permission],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
[SessionRunnerModel.node, models],
[SystemContextBuiltIns.node, systemContext],
[InstructionContext.node, instructionContext],
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
[SkillGuidance.node, skillGuidance],
[ReferenceGuidance.node, referenceGuidance],
[Config.node, config],
[Snapshot.node, Snapshot.noopLayer],
[SessionExecution.node, execution],
],
),
)
const sessionID = SessionV2.ID.make("ses_runner_recorded")
describe("SessionRunnerLLM recorded", () => {
it.effect("executes one recorded V2 prompt through the recorded HTTP transport", () =>
Effect.gen(function* () {
const { db } = yield* Database.Service
yield* db
.insert(ProjectTable)
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
.onConflictDoNothing()
.run()
.pipe(Effect.orDie)
yield* db
.insert(SessionTable)
.values({
id: sessionID,
project_id: Project.ID.global,
slug: "test",
directory: "/project",
title: "test",
version: "test",
})
.onConflictDoNothing()
.run()
.pipe(Effect.orDie)
const session = yield* SessionV2.Service
const prompt = yield* session.prompt({
sessionID,
prompt: Prompt.make({ text: "Say hello in one short sentence." }),
resume: false,
})
yield* session.resume(sessionID)
const messages = yield* session.context(sessionID)
expect(messages).toHaveLength(2)
expect(messages[0]).toMatchObject({ id: prompt.id, type: "user", text: "Say hello in one short sentence." })
expect(messages[1]).toMatchObject({ type: "assistant", agent: "build", finish: "stop" })
expect(messages[1]?.type === "assistant" ? messages[1].content : []).toMatchObject([
{ type: "text", text: "Hello!" },
])
expect(
(yield* db
.select({ type: EventTable.type })
.from(EventTable)
.where(eq(EventTable.aggregate_id, sessionID))
.orderBy(EventTable.seq)
.all()).map((event) => event.type),
).toEqual([
"session.next.prompt.admitted.1",
"session.next.prompted.1",
"session.next.step.started.1",
"session.next.text.started.1",
"session.next.text.ended.1",
"session.next.step.ended.2",
])
}),
)
})