| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- import { Flag } from "@kirincode-ai/core/flag/flag"
- import { ConfigV1 } from "@kirincode-ai/core/v1/config/config"
- import { SessionV1 } from "@kirincode-ai/core/v1/session"
- import { Cause, Duration, Effect, Layer, Scope } from "effect"
- import { TestLLMServer } from "../../lib/llm-server"
- import type { Config } from "../../../src/config/config"
- import type { MessageV2 } from "../../../src/session/message-v2"
- import { MessageID, PartID } from "../../../src/session/schema"
- import { call, callAuthProbe, disposeApps } from "./backend"
- import { original } from "./environment"
- import { runtime } from "./runtime"
- import type { ActiveScenario, Options, ProjectOptions, Result, Scenario, ScenarioContext, SeededContext } from "./types"
- import { ProviderV2 } from "@kirincode-ai/core/provider"
- import { ModelV2 } from "@kirincode-ai/core/model"
- export function runScenario(options: Options) {
- return (scenario: Scenario) => {
- if (scenario.kind === "todo") return Effect.succeed({ status: "skip", scenario } as Result)
- return runActive(options, scenario).pipe(
- Effect.timeoutOrElse({
- duration: options.scenarioTimeout,
- orElse: () => Effect.die(new Error(`scenario timed out after ${Duration.format(options.scenarioTimeout)}`)),
- }),
- Effect.as({ status: "pass", scenario } as Result),
- Effect.catchCause((cause) => Effect.succeed({ status: "fail" as const, scenario, message: Cause.pretty(cause) })),
- Effect.scoped,
- )
- }
- }
- function runActive(options: Options, scenario: ActiveScenario) {
- if (options.mode === "auth") return runAuth(scenario)
- return withContext(options, scenario, "shared", (ctx) =>
- Effect.gen(function* () {
- yield* trace(options, scenario, "request start")
- const result = yield* call(scenario, ctx)
- yield* trace(options, scenario, `response ${result.status}`)
- yield* trace(options, scenario, "expect start")
- yield* scenario.expect(ctx, ctx.state, result)
- yield* trace(options, scenario, "expect done")
- }),
- )
- }
- function runAuth(scenario: ActiveScenario) {
- return Effect.gen(function* () {
- const result = yield* callAuthProbe(scenario, "missing")
- if (scenario.auth === "protected") {
- if (result.status !== 401) throw new Error(`auth expected 401, got ${result.status}`)
- const authed = yield* callAuthProbe(scenario, "valid")
- if (authed.status === 401) throw new Error("auth rejected valid credentials")
- return
- }
- if (result.status === 401) throw new Error("auth expected public access, got 401")
- if (result.timedOut) throw new Error("auth expected public access, probe timed out")
- })
- }
- function withContext<A, E>(
- options: Options,
- scenario: ActiveScenario,
- label: string,
- use: (ctx: SeededContext<unknown>) => Effect.Effect<A, E>,
- ) {
- return Effect.acquireRelease(
- Effect.gen(function* () {
- yield* trace(options, scenario, `${label} context acquire start`)
- const llm = scenario.project?.llm ? yield* TestLLMServer : undefined
- const project = scenario.project
- const dir = project
- ? yield* Effect.promise(async () => (await runtime()).tmpdir(projectOptions(project, llm?.url)))
- : undefined
- yield* trace(options, scenario, `${label} context acquire done`)
- return { dir, llm }
- }),
- (ctx) =>
- Effect.gen(function* () {
- yield* trace(options, scenario, `${label} tmpdir cleanup start`)
- yield* Effect.promise(async () => {
- await ctx.dir?.[Symbol.asyncDispose]()
- }).pipe(Effect.ignore)
- yield* trace(options, scenario, `${label} tmpdir cleanup done`)
- }),
- ).pipe(
- Effect.flatMap((context) =>
- Effect.gen(function* () {
- yield* trace(options, scenario, `${label} runtime start`)
- const modules = yield* Effect.promise(() => runtime())
- const scope = yield* Scope.Scope
- const app = yield* Layer.buildWithMemoMap(modules.AppLayer, modules.memoMap, scope)
- yield* trace(options, scenario, `${label} runtime done`)
- const path = context.dir?.path
- const instance = path
- ? yield* trace(options, scenario, `${label} instance load start`).pipe(
- Effect.andThen(
- modules.InstanceStore.Service.use((store) => store.load({ directory: path })).pipe(
- Effect.provide(app),
- Effect.catchCause((cause) =>
- Effect.sleep("100 millis").pipe(
- Effect.andThen(
- modules.InstanceStore.Service.use((store) => store.load({ directory: path })).pipe(
- Effect.provide(app),
- ),
- ),
- Effect.catchCause(() => Effect.failCause(cause)),
- ),
- ),
- ),
- ),
- Effect.tap(() => trace(options, scenario, `${label} instance load done`)),
- )
- : undefined
- const run = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
- effect.pipe(Effect.provideService(modules.InstanceRef, instance), Effect.provide(app))
- const directory = () => {
- if (!context.dir?.path) throw new Error("scenario needs a project directory")
- return context.dir.path
- }
- const llm = () => {
- if (!context.llm) throw new Error("scenario needs fake LLM")
- return context.llm
- }
- const base: ScenarioContext = {
- directory: context.dir?.path,
- headers: (extra) => ({
- ...(context.dir?.path ? { "x-opencode-directory": context.dir.path } : {}),
- ...extra,
- }),
- file: (name, content) =>
- Effect.promise(() => {
- return Bun.write(`${directory()}/${name}`, content)
- }).pipe(Effect.asVoid),
- session: (input) =>
- run(modules.Session.Service.use((svc) => svc.create({ title: input?.title, parentID: input?.parentID }))),
- sessionGet: (sessionID) =>
- run(modules.Session.Service.use((svc) => svc.get(sessionID))).pipe(
- Effect.catchCause(() => Effect.succeed(undefined)),
- ),
- project: () =>
- Effect.sync(() => {
- if (!instance) throw new Error("scenario needs a project directory")
- return instance.project
- }),
- message: (sessionID, input) =>
- Effect.gen(function* () {
- const info: SessionV1.User = {
- id: MessageID.ascending(),
- sessionID,
- role: "user",
- time: { created: Date.now() },
- agent: "build",
- model: {
- providerID: ProviderV2.ID.kirincode,
- modelID: ModelV2.ID.make("test"),
- },
- }
- const part: SessionV1.TextPart = {
- id: PartID.ascending(),
- sessionID,
- messageID: info.id,
- type: "text",
- text: input?.text ?? "hello",
- }
- yield* run(
- modules.Session.Service.use((svc) =>
- Effect.gen(function* () {
- yield* svc.updateMessage(info)
- yield* svc.updatePart(part)
- }),
- ),
- )
- return { info, part }
- }),
- messages: (sessionID) =>
- run(modules.Session.Service.use((svc) => svc.messages({ sessionID }).pipe(Effect.orDie))),
- todos: (sessionID, todos) => run(modules.Todo.Service.use((svc) => svc.update({ sessionID, todos }))),
- worktree: (input) => run(modules.Worktree.Service.use((svc) => svc.create(input).pipe(Effect.orDie))),
- worktreeRemove: (directory) =>
- run(modules.Worktree.Service.use((svc) => svc.remove({ directory })).pipe(Effect.ignore)),
- llmText: (value) => Effect.suspend(() => llm().text(value)),
- llmWait: (count) => Effect.suspend(() => llm().wait(count)),
- tuiRequest: (request) => Effect.sync(() => modules.Tui.submitTuiRequest(request)),
- }
- yield* trace(options, scenario, `${label} seed start`)
- const state = yield* scenario.seed(base)
- yield* trace(options, scenario, `${label} seed done`)
- yield* trace(options, scenario, `${label} use start`)
- const result = yield* use({ ...base, state })
- yield* trace(options, scenario, `${label} use done`)
- return result
- }).pipe(Effect.ensuring(context.llm ? context.llm.reset : Effect.void)),
- ),
- Effect.ensuring(scenario.reset ? resetState : Effect.void),
- )
- }
- function trace(options: Options, scenario: ActiveScenario, phase: string) {
- return Effect.sync(() => {
- if (!options.trace) return
- console.log(`[trace] ${scenario.name}: ${phase}`)
- })
- }
- function projectOptions(
- project: ProjectOptions,
- llmUrl: string | undefined,
- ): { git?: boolean; config?: Partial<ConfigV1.Info> } {
- if (!project.llm || !llmUrl) return { git: project.git, config: project.config }
- const fake = fakeLlmConfig(llmUrl)
- return {
- git: project.git,
- config: {
- ...fake,
- ...project.config,
- provider: {
- ...fake.provider,
- ...project.config?.provider,
- },
- },
- }
- }
- function fakeLlmConfig(url: string): Partial<ConfigV1.Info> {
- return {
- model: "test/test-model",
- small_model: "test/test-model",
- provider: {
- test: {
- name: "Test",
- id: "test",
- env: [],
- npm: "@ai-sdk/openai-compatible",
- models: {
- "test-model": {
- id: "test-model",
- name: "Test Model",
- attachment: false,
- reasoning: false,
- temperature: false,
- tool_call: true,
- release_date: "2025-01-01",
- limit: { context: 100000, output: 10000 },
- cost: { input: 0, output: 0 },
- options: {},
- },
- },
- options: {
- apiKey: "test-key",
- baseURL: url,
- },
- },
- },
- }
- }
- const resetState = Effect.promise(async () => {
- const modules = await runtime()
- Flag.KIRINCODE_SERVER_PASSWORD = original.KIRINCODE_SERVER_PASSWORD
- Flag.KIRINCODE_SERVER_USERNAME = original.KIRINCODE_SERVER_USERNAME
- await disposeApps()
- await modules.disposeAllInstances()
- await modules.resetDatabase()
- await Bun.sleep(25)
- })
|