mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-24 10:59:56 +00:00
196 lines
7.8 KiB
TypeScript
196 lines
7.8 KiB
TypeScript
import { expect } from "bun:test"
|
|
import { Bus } from "@opencode/core/bus"
|
|
import { Database } from "@opencode/core/database/database"
|
|
import { Plugin } from "@opencode/core/plugin"
|
|
import { PluginHost } from "@opencode/core/plugin/host"
|
|
import { PluginPromise } from "@opencode/core/plugin/promise"
|
|
import { Session } from "@opencode/core/session"
|
|
import { SessionHistory } from "@opencode/core/session/history"
|
|
import { InstructionEntry } from "@opencode/core/session/instruction-entry"
|
|
import { InstructionState } from "@opencode/core/session/instruction-state"
|
|
import { Effect } from "effect"
|
|
import { testEffect } from "../lib/effect"
|
|
import { PluginTestLayer } from "./fixture"
|
|
|
|
const it = testEffect(PluginTestLayer)
|
|
|
|
const prepareRequest = Effect.fnUntraced(function* (sessionID: Session.ID) {
|
|
const { db } = yield* Database.Service
|
|
const bus = yield* Bus.Service
|
|
const entries = yield* InstructionEntry.Service
|
|
const instructions = yield* entries.load(sessionID)
|
|
yield* InstructionState.prepare(db, bus, instructions, sessionID)
|
|
return yield* SessionHistory.entriesForRunner(db, sessionID, instructions, "local")
|
|
})
|
|
|
|
it.live("Effect plugins can put, list, and remove session instruction entries seen by the next model request", () =>
|
|
Effect.gen(function* () {
|
|
const plugins = yield* Plugin.Service
|
|
const sessions = yield* Session.Service
|
|
const context = yield* PluginHost.make(plugins)
|
|
const created = yield* context.session.create({ title: "Plugin instructions" })
|
|
|
|
yield* context.session.instructions.entry.put({
|
|
sessionID: created.id,
|
|
key: "deploy-target",
|
|
value: "production",
|
|
})
|
|
expect(yield* context.session.instructions.entry.list({ sessionID: created.id })).toEqual([
|
|
{ key: "deploy-target", value: "production" },
|
|
])
|
|
expect(yield* sessions.inbox(created.id)).toEqual([])
|
|
|
|
const first = yield* prepareRequest(created.id)
|
|
expect(first.initial).toBe(['<context key="deploy-target">', "production", "</context>"].join("\n"))
|
|
expect(first.entries).toEqual([])
|
|
|
|
yield* context.session.instructions.entry.put({
|
|
sessionID: created.id,
|
|
key: "deploy-target",
|
|
value: { region: "us-east-1" },
|
|
})
|
|
const second = yield* prepareRequest(created.id)
|
|
expect(second.entries.map((entry) => entry.message)).toEqual([
|
|
expect.objectContaining({
|
|
type: "system",
|
|
text: [
|
|
'The context under "deploy-target" changed and supersedes the previous value:',
|
|
'<context key="deploy-target">',
|
|
"{",
|
|
' "region": "us-east-1"',
|
|
"}",
|
|
"</context>",
|
|
].join("\n"),
|
|
}),
|
|
])
|
|
|
|
yield* context.session.instructions.entry.remove({ sessionID: created.id, key: "deploy-target" })
|
|
expect(yield* context.session.instructions.entry.list({ sessionID: created.id })).toEqual([])
|
|
|
|
const third = yield* prepareRequest(created.id)
|
|
expect(third.entries.at(-1)?.message).toMatchObject({
|
|
type: "system",
|
|
text: 'The context under "deploy-target" no longer applies. Disregard it.',
|
|
})
|
|
expect(yield* sessions.inbox(created.id)).toEqual([])
|
|
|
|
const missing = Session.ID.create()
|
|
for (const operation of [
|
|
context.session.instructions.entry.list({ sessionID: missing }),
|
|
context.session.instructions.entry.put({ sessionID: missing, key: "deploy-target", value: "production" }),
|
|
context.session.instructions.entry.remove({ sessionID: missing, key: "deploy-target" }),
|
|
]) {
|
|
expect(yield* operation.pipe(Effect.flip)).toMatchObject({
|
|
_tag: "Session.NotFoundError",
|
|
sessionID: missing,
|
|
})
|
|
}
|
|
expect(
|
|
yield* context.session.instructions.entry
|
|
.put({
|
|
sessionID: created.id,
|
|
key: "oversized",
|
|
value: "x".repeat(InstructionEntry.MaxValueBytes),
|
|
})
|
|
.pipe(Effect.flip),
|
|
).toBeInstanceOf(InstructionEntry.ValueTooLargeError)
|
|
|
|
for (const key of ["Bad Key", "invalid/key"]) {
|
|
for (const operation of [
|
|
context.session.instructions.entry.put({ sessionID: created.id, key, value: "rejected" }),
|
|
context.session.instructions.entry.remove({ sessionID: created.id, key }),
|
|
]) {
|
|
expect(yield* operation.pipe(Effect.flip)).toMatchObject({ _tag: "SchemaError" })
|
|
}
|
|
}
|
|
yield* context.session.instructions.entry.put({
|
|
sessionID: created.id,
|
|
key: "recovered",
|
|
value: "still works",
|
|
})
|
|
const fourth = yield* prepareRequest(created.id)
|
|
expect(fourth.entries.at(-1)?.message).toMatchObject({
|
|
type: "system",
|
|
text: ['<context key="recovered">', "still works", "</context>"].join("\n"),
|
|
})
|
|
}),
|
|
)
|
|
|
|
it.live("Promise plugins can put, list, and remove session instruction entries seen by the next model request", () =>
|
|
Effect.gen(function* () {
|
|
const plugins = yield* Plugin.Service
|
|
const sessions = yield* Session.Service
|
|
const context = yield* PluginHost.make(plugins)
|
|
const created = yield* context.session.create({ title: "Promise plugin instructions" })
|
|
|
|
yield* PluginPromise.fromPromise({
|
|
id: "test.session-instructions.put",
|
|
async setup(ctx) {
|
|
expect(
|
|
await ctx.session.instructions.entry.put({
|
|
sessionID: created.id,
|
|
key: "genui.guidelines",
|
|
value: "Use GenUI components when appropriate.",
|
|
}),
|
|
).toBeUndefined()
|
|
expect(await ctx.session.instructions.entry.list({ sessionID: created.id })).toEqual([
|
|
{ key: "genui.guidelines", value: "Use GenUI components when appropriate." },
|
|
])
|
|
const missing = Session.ID.create()
|
|
await expect(ctx.session.instructions.entry.list({ sessionID: missing })).rejects.toMatchObject({
|
|
_tag: "Session.NotFoundError",
|
|
sessionID: missing,
|
|
})
|
|
await expect(
|
|
ctx.session.instructions.entry.put({ sessionID: missing, key: "deploy-target", value: "production" }),
|
|
).rejects.toMatchObject({ _tag: "Session.NotFoundError", sessionID: missing })
|
|
await expect(
|
|
ctx.session.instructions.entry.remove({ sessionID: missing, key: "deploy-target" }),
|
|
).rejects.toMatchObject({ _tag: "Session.NotFoundError", sessionID: missing })
|
|
await expect(
|
|
ctx.session.instructions.entry.put({
|
|
sessionID: created.id,
|
|
key: "invalid/key",
|
|
value: "rejected",
|
|
}),
|
|
).rejects.toMatchObject({ _tag: "SchemaError" })
|
|
await expect(
|
|
ctx.session.instructions.entry.remove({ sessionID: created.id, key: "invalid/key" }),
|
|
).rejects.toMatchObject({ _tag: "SchemaError" })
|
|
await expect(
|
|
ctx.session.instructions.entry.put({
|
|
sessionID: created.id,
|
|
key: "oversized",
|
|
value: "x".repeat(InstructionEntry.MaxValueBytes),
|
|
}),
|
|
).rejects.toBeInstanceOf(InstructionEntry.ValueTooLargeError)
|
|
},
|
|
}).effect(context)
|
|
|
|
const first = yield* prepareRequest(created.id)
|
|
expect(first.initial).toBe(
|
|
['<context key="genui.guidelines">', "Use GenUI components when appropriate.", "</context>"].join("\n"),
|
|
)
|
|
expect(yield* sessions.inbox(created.id)).toEqual([])
|
|
|
|
yield* PluginPromise.fromPromise({
|
|
id: "test.session-instructions.remove",
|
|
async setup(ctx) {
|
|
expect(
|
|
await ctx.session.instructions.entry.remove({
|
|
sessionID: created.id,
|
|
key: "genui.guidelines",
|
|
}),
|
|
).toBeUndefined()
|
|
expect(await ctx.session.instructions.entry.list({ sessionID: created.id })).toEqual([])
|
|
},
|
|
}).effect(context)
|
|
|
|
const second = yield* prepareRequest(created.id)
|
|
expect(second.entries.at(-1)?.message).toMatchObject({
|
|
type: "system",
|
|
text: 'The context under "genui.guidelines" no longer applies. Disregard it.',
|
|
})
|
|
expect(yield* sessions.inbox(created.id)).toEqual([])
|
|
}),
|
|
)
|