Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion packages/opencode/test/session/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ const blockingProcessor = Layer.succeed(
}),
)

const providerGetModelStarted: Array<() => void> = []
const blockingProvider = Layer.mock(ProviderSvc.Service, {
getModel: () => Effect.sync(() => providerGetModelStarted.shift()?.()).pipe(Effect.andThen(Effect.never)),
})

const runtimeFlags = RuntimeFlags.layer({ experimentalEventSystem: true })

const testLLMServerNode = LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })
Expand Down Expand Up @@ -208,7 +213,11 @@ const promptRoot = LayerNode.group([
RuntimeFlags.node,
])

function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) {
function makePrompt(input?: {
mcpInstructions?: MCP.ServerInstructions[]
processor?: "blocking"
provider?: "blocking"
}) {
const replacements = [
[SessionSummary.node, summary],
[LSP.node, lsp],
Expand All @@ -218,6 +227,9 @@ function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; proces
if (input?.processor === "blocking") {
return LayerNode.compile(promptRoot, [...replacements, [SessionProcessor.node, blockingProcessor]])
}
if (input?.provider === "blocking") {
return LayerNode.compile(promptRoot, [...replacements, [ProviderSvc.node, blockingProvider]])
}
return LayerNode.compile(promptRoot, replacements)
}

Expand All @@ -242,6 +254,7 @@ function makeHttpNoLLMServer(input?: { mcpInstructions?: MCP.ServerInstructions[
const it = testEffect(makeHttp())
const noLLMServer = testEffect(makeHttpNoLLMServer())
const raceNoLLMServer = testEffect(makeHttpNoLLMServer({ processor: "blocking" }))
const rolloverNoLLMServer = testEffect(makePrompt({ provider: "blocking" }))
const withMcpInstructions = testEffect(
makeHttp({
mcpInstructions: [
Expand Down Expand Up @@ -500,6 +513,73 @@ noLLMServer.instance(
{ config: cfg },
)

rolloverNoLLMServer.instance(
"loop starts a new user turn after message ID rollover",
() =>
Effect.gen(function* () {
providerGetModelStarted.length = 0
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
providerGetModelStarted.length = 0
}),
)

const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const chat = yield* sessions.create({ title: "Pinned" })
const oldUserID = MessageID.make("msg_ffffffffe001olduser")
const oldAssistantID = MessageID.make("msg_fffffffff001oldassistant")
const newUserID = MessageID.make("msg_000000001001newuser")
yield* sessions.updateMessage({
id: oldUserID,
role: "user",
sessionID: chat.id,
agent: "build",
model: ref,
time: { created: 100 },
})
yield* sessions.updateMessage({
id: oldAssistantID,
role: "assistant",
parentID: oldUserID,
sessionID: chat.id,
mode: "build",
agent: "build",
cost: 0,
path: { cwd: "/tmp", root: "/tmp" },
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
modelID: ref.modelID,
providerID: ref.providerID,
time: { created: 200, completed: 201 },
finish: "stop",
})
yield* sessions.updateMessage({
id: newUserID,
role: "user",
sessionID: chat.id,
agent: "build",
model: ref,
time: { created: 300 },
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: newUserID,
sessionID: chat.id,
type: "text",
text: "continue after rollover",
})
const latest = MessageV2.latest(yield* sessions.messages({ sessionID: chat.id }))
expect(latest.user?.id).toBe(newUserID)
expect(latest.assistant?.id).toBe(oldAssistantID)

const started = defer<void>()
providerGetModelStarted.push(started.resolve)
const loop = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
yield* awaitWithTimeout(Effect.promise(() => started.promise), "rollover turn never started")
yield* Fiber.interrupt(loop)
}),
)

it.instance("loop exits without an LLM request for interrupted orphan tool calls", () =>
Effect.gen(function* () {
const { llm } = yield* useServerConfig(providerCfg)
Expand Down
Loading