mirror of
https://github.com/Heretek-AI/opencode.git
synced 2026-11-07 10:56:09 -10:00
test: trim overlapping tool input coverage
This commit is contained in:
1 parent
600c40e1b8
commit
3570dab95f
3 files changed
+21
-168
No files matched your search
@@ -1929,44 +1929,6 @@ describe("Anthropic Messages route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
for (const stopReason of ["tool_use", "max_tokens", "refusal"]) {
|
||||
it.effect(`rejects incomplete local tool input before ${stopReason}`, () =>
|
||||
Effect.gen(function* () {
|
||||
const raw = '{"query":"partial'
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
sseEvents(
|
||||
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
|
||||
{
|
||||
type: "content_block_start",
|
||||
index: 0,
|
||||
content_block: { type: "tool_use", id: "call_1", name: "lookup" },
|
||||
},
|
||||
{ type: "content_block_delta", index: 0, delta: { type: "input_json_delta", partial_json: raw } },
|
||||
{ type: "content_block_stop", index: 0 },
|
||||
{ type: "message_delta", delta: { stop_reason: stopReason }, usage: { output_tokens: 1 } },
|
||||
{ type: "message_stop" },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(response.toolCalls).toEqual([])
|
||||
expect(response.events.find((event) => event.type === "tool-input-delta")).toMatchObject({
|
||||
text: raw,
|
||||
input: { query: "partial" },
|
||||
})
|
||||
expect(response.events.find((event) => event.type === "tool-input-error")).toEqual({
|
||||
type: "tool-input-error",
|
||||
id: "call_1",
|
||||
name: "lookup",
|
||||
raw,
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
it.effect("keeps malformed server tool input terminal", () =>
|
||||
Effect.gen(function* () {
|
||||
const body = sseEvents(
|
||||
|
||||
@@ -145,37 +145,6 @@ describe("ToolStream", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects malformed authoritative input instead of using valid deltas", () =>
|
||||
Effect.gen(function* () {
|
||||
const tools = ToolStream.start(ToolStream.empty<string>(), "item_1", {
|
||||
id: "call_1",
|
||||
name: "lookup",
|
||||
input: '{"query":"weather"}',
|
||||
})
|
||||
const finished = yield* ToolStream.finishWithInput(ADAPTER, tools, "item_1", '{"query":"partial')
|
||||
|
||||
expect(finished).toEqual({
|
||||
tools: {},
|
||||
events: [
|
||||
{ type: "tool-input-end", id: "call_1", name: "lookup" },
|
||||
{ type: "tool-input-error", id: "call_1", name: "lookup", raw: '{"query":"partial' },
|
||||
],
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves empty input for zero-argument tools", () =>
|
||||
Effect.gen(function* () {
|
||||
const tools = ToolStream.start(ToolStream.empty<number>(), 0, { id: "call_1", name: "lookup" })
|
||||
const finished = yield* ToolStream.finish(ADAPTER, tools, 0)
|
||||
|
||||
expect(finished.events).toEqual([
|
||||
{ type: "tool-input-end", id: "call_1", name: "lookup" },
|
||||
{ type: "tool-call", id: "call_1", name: "lookup", input: {} },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects malformed string escapes in final local input", () =>
|
||||
Effect.gen(function* () {
|
||||
const tools = ToolStream.start(ToolStream.empty<string>(), "item_1", {
|
||||
|
||||
@@ -3721,45 +3721,6 @@ describe("SessionRunnerLLM", () => {
|
||||
expect(systemTexts(s.requests[1])).toContain("Replacement context")
|
||||
})
|
||||
|
||||
scenario("executes a completed tool call before the provider finishes the response", function* (s) {
|
||||
yield* s.admit("Echo this")
|
||||
const tools = yield* s.blockTools()
|
||||
const finish = yield* Deferred.make<void>()
|
||||
yield* s.llm.push(
|
||||
Stream.fromIterable([
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.toolInputStart({ id: "call-early", name: "echo" }),
|
||||
LLMEvent.toolInputDelta({ id: "call-early", name: "echo", text: '{"text":"hello"}' }),
|
||||
LLMEvent.toolInputEnd({ id: "call-early", name: "echo" }),
|
||||
LLMEvent.toolCall({ id: "call-early", name: "echo", input: { text: "hello" } }),
|
||||
]).pipe(
|
||||
Stream.concat(
|
||||
Stream.fromEffect(Deferred.await(finish)).pipe(
|
||||
Stream.flatMap(() =>
|
||||
Stream.fromIterable([
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "tool-calls" } }),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TestLLM.stop(),
|
||||
)
|
||||
const run = yield* s.resume.pipe(Effect.forkChild)
|
||||
yield* tools.started
|
||||
|
||||
expect(requireAssistant(yield* s.context)).toMatchObject({
|
||||
content: [{ type: "tool", id: "call-early", state: { status: "running", input: { text: "hello" } } }],
|
||||
})
|
||||
expect(yield* recordedEventTypes(sessionID)).not.toContain("session.step.streamed.1")
|
||||
|
||||
yield* Deferred.succeed(finish, undefined)
|
||||
yield* tools.release
|
||||
yield* Fiber.join(run)
|
||||
expect(s.executions).toEqual(["hello"])
|
||||
})
|
||||
|
||||
scenario("consumes the full provider stream before recording its boundary and settling local tools", function* (s) {
|
||||
yield* s.admit("Echo this")
|
||||
const request = yield* s.llm.gate
|
||||
@@ -6071,68 +6032,29 @@ describe("SessionRunnerLLM", () => {
|
||||
])
|
||||
})
|
||||
|
||||
for (const finish of ["stop", "length"] as const) {
|
||||
for (const ended of [false, true]) {
|
||||
scenario(`settles unfinished tool input after ${finish} (input ended: ${ended})`, function* (s) {
|
||||
const raw = '{"text":"unfinished-marker'
|
||||
yield* s.llm.push(
|
||||
TestLLM.complete(
|
||||
{ reason: { normalized: finish } },
|
||||
LLMEvent.toolCall({ id: "call-valid", name: "echo", input: { text: "valid" } }),
|
||||
LLMEvent.toolInputStart({ id: "call-incomplete", name: "echo" }),
|
||||
LLMEvent.toolInputDelta({ id: "call-incomplete", name: "echo", text: raw }),
|
||||
...(ended ? [LLMEvent.toolInputEnd({ id: "call-incomplete", name: "echo" })] : []),
|
||||
),
|
||||
TestLLM.stop(),
|
||||
)
|
||||
scenario("settles unfinished tool input after an output limit", function* (s) {
|
||||
yield* s.llm.push(
|
||||
TestLLM.complete(
|
||||
{ reason: { normalized: "length" } },
|
||||
LLMEvent.toolInputStart({ id: "call-incomplete", name: "echo" }),
|
||||
LLMEvent.toolInputDelta({ id: "call-incomplete", name: "echo", text: '{"text":"partial' }),
|
||||
),
|
||||
TestLLM.stop(),
|
||||
)
|
||||
|
||||
yield* s.runPrompt("Recover unfinished tool input")
|
||||
yield* s.runPrompt("Recover unfinished tool input")
|
||||
|
||||
expect(s.requests).toHaveLength(2)
|
||||
expect(s.executions).toEqual(["valid"])
|
||||
expect(JSON.stringify(s.requests[1])).not.toContain("unfinished-marker")
|
||||
expect(s.requests[1]?.messages).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
role: "tool",
|
||||
content: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "tool-result",
|
||||
id: "call-incomplete",
|
||||
result: expect.objectContaining({ type: "error" }),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
const assistant = requireAssistant(yield* s.context)
|
||||
expect(assistant.error).toBeUndefined()
|
||||
expect(assistant.content).toMatchObject([
|
||||
{ type: "tool", id: "call-valid", state: { status: "completed" } },
|
||||
{
|
||||
type: "tool",
|
||||
id: "call-incomplete",
|
||||
executed: false,
|
||||
state: { status: "error", error: { type: "tool.input-incomplete" } },
|
||||
},
|
||||
])
|
||||
const events = yield* s.db
|
||||
.select({ type: EventTable.type, data: EventTable.data })
|
||||
.from(EventTable)
|
||||
.where(eq(EventTable.aggregate_id, sessionID))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
expect(
|
||||
events.filter((event) => event.type === "session.tool.input.ended.1" && event.data.id === "call-incomplete"),
|
||||
).toHaveLength(1)
|
||||
expect(
|
||||
events.filter((event) => event.type === "session.tool.failed.2" && event.data.id === "call-incomplete"),
|
||||
).toHaveLength(1)
|
||||
yield* replaySessionProjection(sessionID)
|
||||
expect((yield* s.context).find((message) => message.id === assistant.id)).toEqual(assistant)
|
||||
})
|
||||
}
|
||||
}
|
||||
expect(s.requests).toHaveLength(2)
|
||||
expect(s.executions).toEqual([])
|
||||
expect(requireAssistant(yield* s.context).content).toMatchObject([
|
||||
{
|
||||
type: "tool",
|
||||
id: "call-incomplete",
|
||||
executed: false,
|
||||
state: { status: "error", error: { type: "tool.input-incomplete" } },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
scenario("continues after malformed local tool input without exposing raw arguments", function* (s) {
|
||||
const marker = "raw-malformed-marker"
|
||||
|
||||
Reference in new issue
Block a user