| ... | ... | @@ -21,6 +21,7 @@ import type { |
| 21 | 21 | Event as OpenCodeEvent, |
| 22 | 22 | PermissionRequest, |
| 23 | 23 | QuestionRequest, |
| 24 | ToolPart, |
| 24 | 25 | } from "@opencode-ai/sdk/v2"; |
| 25 | 26 | |
| 26 | 27 | import { |
| ... | ... | @@ -6536,6 +6537,133 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { |
| 6536 | 6537 | }), |
| 6537 | 6538 | ); |
| 6538 | 6539 | |
| 6540 | it.effect("emits tool lifecycle events before late assistant metadata", () => |
| 6541 | Effect.gen(function* () { |
| 6542 | const adapter = yield* OpenCodeAdapter; |
| 6543 | const threadId = asThreadId("thread-tool-lifecycle"); |
| 6544 | const sessionID = "http://127.0.0.1:9999/session"; |
| 6545 | const messageID = "msg-tools-before-role"; |
| 6546 | const start = promiseWithResolvers<OpenCodeEvent>(); |
| 6547 | const input = { command: "pwd" }; |
| 6548 | const states = [ |
| 6549 | { status: "pending", input, raw: "" }, |
| 6550 | { status: "running", input, title: "Working directory", time: { start: 1 } }, |
| 6551 | { |
| 6552 | status: "completed", |
| 6553 | input, |
| 6554 | output: "/repo\n", |
| 6555 | title: "Working directory", |
| 6556 | metadata: {}, |
| 6557 | time: { start: 1, end: 2 }, |
| 6558 | }, |
| 6559 | { status: "error", input, error: "Command failed", time: { start: 3, end: 4 } }, |
| 6560 | ] satisfies ReadonlyArray<ToolPart["state"]>; |
| 6561 | runtimeMock.state.subscribedEvents = [ |
| 6562 | start.promise, |
| 6563 | ...states.map( |
| 6564 | (state) => |
| 6565 | ({ |
| 6566 | id: `evt-tool-${state.status}`, |
| 6567 | type: "message.part.updated", |
| 6568 | properties: { |
| 6569 | sessionID, |
| 6570 | time: 4, |
| 6571 | part: { |
| 6572 | id: state.status === "error" ? "part-failed" : "part-working", |
| 6573 | sessionID, |
| 6574 | messageID, |
| 6575 | type: "tool", |
| 6576 | callID: state.status === "error" ? "call-failed" : "call-working", |
| 6577 | tool: "bash", |
| 6578 | state, |
| 6579 | }, |
| 6580 | }, |
| 6581 | }) satisfies OpenCodeEvent, |
| 6582 | ), |
| 6583 | { |
| 6584 | id: "evt-text-before-role", |
| 6585 | type: "message.part.updated", |
| 6586 | properties: { |
| 6587 | sessionID, |
| 6588 | time: 4, |
| 6589 | part: { |
| 6590 | id: "part-late-text", |
| 6591 | sessionID, |
| 6592 | messageID, |
| 6593 | type: "text", |
| 6594 | text: "Tool results received", |
| 6595 | time: { start: 4 }, |
| 6596 | }, |
| 6597 | }, |
| 6598 | }, |
| 6599 | { |
| 6600 | id: "evt-late-assistant-role", |
| 6601 | type: "message.updated", |
| 6602 | properties: { sessionID, info: { id: messageID, role: "assistant" } }, |
| 6603 | }, |
| 6604 | { |
| 6605 | id: "evt-tool-lifecycle-drained", |
| 6606 | type: "session.compacted", |
| 6607 | properties: { sessionID }, |
| 6608 | }, |
| 6609 | ]; |
| 6610 | const eventsFiber = yield* adapter.streamEvents.pipe( |
| 6611 | Stream.filter((event) => event.threadId === threadId), |
| 6612 | Stream.takeUntil((event) => event.type === "thread.state.changed"), |
| 6613 | Stream.runCollect, |
| 6614 | Effect.forkChild, |
| 6615 | ); |
| 6616 | yield* adapter.startSession({ |
| 6617 | provider: ProviderDriverKind.make("opencode"), |
| 6618 | threadId, |
| 6619 | runtimeMode: "full-access", |
| 6620 | }); |
| 6621 | yield* adapter.sendTurn({ |
| 6622 | threadId, |
| 6623 | input: "Read the working directory", |
| 6624 | modelSelection: createModelSelection( |
| 6625 | ProviderInstanceId.make("opencode"), |
| 6626 | "opencode/kimi-k3", |
| 6627 | ), |
| 6628 | }); |
| 6629 | start.resolve({ |
| 6630 | id: "evt-tool-lifecycle-started", |
| 6631 | type: "session.status", |
| 6632 | properties: { sessionID, status: { type: "busy" } }, |
| 6633 | }); |
| 6634 | const events = yield* Fiber.join(eventsFiber); |
| 6635 | const tools = events.filter( |
| 6636 | (event) => |
| 6637 | event.type === "item.started" || |
| 6638 | event.type === "item.updated" || |
| 6639 | event.type === "item.completed", |
| 6640 | ); |
| 6641 | NodeAssert.deepEqual( |
| 6642 | tools.map((event) => [event.type, event.itemId, event.payload.status]), |
| 6643 | [ |
| 6644 | ["item.started", "call-working", "inProgress"], |
| 6645 | ["item.updated", "call-working", "inProgress"], |
| 6646 | ["item.completed", "call-working", "completed"], |
| 6647 | ["item.completed", "call-failed", "failed"], |
| 6648 | ], |
| 6649 | ); |
| 6650 | NodeAssert.partialDeepStrictEqual(tools[2]?.payload.data, { |
| 6651 | command: "pwd", |
| 6652 | result: "/repo\n", |
| 6653 | }); |
| 6654 | NodeAssert.partialDeepStrictEqual(tools[3]?.payload.data, { |
| 6655 | command: "pwd", |
| 6656 | state: { error: "Command failed" }, |
| 6657 | }); |
| 6658 | NodeAssert.deepEqual( |
| 6659 | events |
| 6660 | .filter((event) => event.type === "content.delta") |
| 6661 | .map((event) => event.payload.delta), |
| 6662 | ["Tool results received"], |
| 6663 | ); |
| 6664 | }), |
| 6665 | ); |
| 6666 | |
| 6539 | 6667 | it.effect("maps native task progress only while a turn is active", () => |
| 6540 | 6668 | Effect.gen(function* () { |
| 6541 | 6669 | const adapter = yield* OpenCodeAdapter; |