| @@ -559,17 +559,98 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { · 558 unmodified lines |
| 559 | 559 | }), |
| 560 | 560 | ); |
| 561 | 561 | |
| 562 | | − it.effect("deduplicates overlapping assistant text deltas after part updates", () => |
| 562 | + it.effect("appends raw assistant text deltas and reconciles part update snapshots", () => |
| 563 | 563 | Effect.sync(() => { |
| 564 | 564 | const firstUpdate = mergeOpenCodeAssistantText(undefined, "Hello"); |
| 565 | 565 | const overlapDelta = appendOpenCodeAssistantTextDelta(firstUpdate.latestText, "lo world"); |
| 566 | | − const secondUpdate = mergeOpenCodeAssistantText(overlapDelta.nextText, "Hello world!"); |
| 566 | + const secondUpdate = mergeOpenCodeAssistantText(overlapDelta.nextText, "Hellolo world"); |
| 567 | 567 | |
| 568 | 568 | assert.deepEqual( |
| 569 | 569 | [firstUpdate.deltaToEmit, overlapDelta.deltaToEmit, secondUpdate.deltaToEmit], |
| 570 | | − ["Hello", " world", "!"], |
| 570 | + ["Hello", "lo world", ""], |
| 571 | 571 | ); |
| 572 | | − assert.equal(secondUpdate.latestText, "Hello world!"); |
| 572 | + assert.equal(secondUpdate.latestText, "Hellolo world"); |
| 573 | + }), |
| 574 | + ); |
| 575 | + |
| 576 | + it.effect("does not strip coincidental prefix overlap from OpenCode part deltas", () => |
| 577 | + Effect.gen(function* () { |
| 578 | + const adapter = yield* OpenCodeAdapter; |
| 579 | + const threadId = asThreadId("thread-opencode-raw-delta"); |
| 580 | + const part = { |
| 581 | + id: "part-raw-delta", |
| 582 | + sessionID: "http://127.0.0.1:9999/session", |
| 583 | + messageID: "msg-raw-delta", |
| 584 | + type: "text", |
| 585 | + text: "A B", |
| 586 | + time: { start: 1 }, |
| 587 | + }; |
| 588 | + runtimeMock.state.subscribedEvents = [ |
| 589 | + { |
| 590 | + type: "message.updated", |
| 591 | + properties: { |
| 592 | + sessionID: "http://127.0.0.1:9999/session", |
| 593 | + info: { |
| 594 | + id: "msg-raw-delta", |
| 595 | + role: "assistant", |
| 596 | + }, |
| 597 | + }, |
| 598 | + }, |
| 599 | + { |
| 600 | + type: "message.part.updated", |
| 601 | + properties: { |
| 602 | + sessionID: "http://127.0.0.1:9999/session", |
| 603 | + part, |
| 604 | + time: 1, |
| 605 | + }, |
| 606 | + }, |
| 607 | + { |
| 608 | + type: "message.part.delta", |
| 609 | + properties: { |
| 610 | + sessionID: "http://127.0.0.1:9999/session", |
| 611 | + messageID: "msg-raw-delta", |
| 612 | + partID: "part-raw-delta", |
| 613 | + field: "text", |
| 614 | + delta: "Bonus", |
| 615 | + }, |
| 616 | + }, |
| 617 | + { |
| 618 | + type: "message.part.updated", |
| 619 | + properties: { |
| 620 | + sessionID: "http://127.0.0.1:9999/session", |
| 621 | + part: { |
| 622 | + ...part, |
| 623 | + text: "A BBonus", |
| 624 | + time: { start: 1, end: 2 }, |
| 625 | + }, |
| 626 | + time: 2, |
| 627 | + }, |
| 628 | + }, |
| 629 | + ]; |
| 630 | + const eventsFiber = yield* adapter.streamEvents.pipe( |
| 631 | + Stream.filter((event) => event.threadId === threadId), |
| 632 | + Stream.take(5), |
| 633 | + Stream.runCollect, |
| 634 | + Effect.forkChild, |
| 635 | + ); |
| 636 | + |
| 637 | + yield* adapter.startSession({ |
| 638 | + provider: ProviderDriverKind.make("opencode"), |
| 639 | + threadId, |
| 640 | + runtimeMode: "full-access", |
| 641 | + }); |
| 642 | + |
| 643 | + const events = Array.from(yield* Fiber.join(eventsFiber).pipe(Effect.timeout("1 second"))); |
| 644 | + const deltas = events.filter((event) => event.type === "content.delta"); |
| 645 | + assert.deepEqual( |
| 646 | + deltas.map((event) => (event.type === "content.delta" ? event.payload.delta : "")), |
| 647 | + ["A B", "Bonus"], |
| 648 | + ); |
| 649 | + assert.equal(events.at(-1)?.type, "item.completed"); |
| 650 | + const completed = events.at(-1); |
| 651 | + if (completed?.type === "item.completed") { |
| 652 | + assert.equal(completed.payload.detail, "A BBonus"); |
| 653 | + } |
| 573 | 654 | }), |
| 574 | 655 | ); |
| 575 | 656 | |