739 lines
21 KiB
TypeScript
739 lines
21 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
||
import { prefixSystemMessage } from "../../infra/system-message.js";
|
||
import { createAcpReplyProjector } from "./acp-projector.js";
|
||
import { createAcpTestConfig as createCfg } from "./test-fixtures/acp-runtime.js";
|
||
|
||
type Delivery = { kind: string; text?: string };
|
||
|
||
function createProjectorHarness(cfgOverrides?: Parameters<typeof createCfg>[0]) {
|
||
const deliveries: Delivery[] = [];
|
||
const projector = createAcpReplyProjector({
|
||
cfg: createCfg(cfgOverrides),
|
||
shouldSendToolSummaries: true,
|
||
deliver: async (kind, payload) => {
|
||
deliveries.push({ kind, text: payload.text });
|
||
return true;
|
||
},
|
||
});
|
||
return { deliveries, projector };
|
||
}
|
||
|
||
function createLiveCfgOverrides(
|
||
streamOverrides: Record<string, unknown>,
|
||
): Parameters<typeof createCfg>[0] {
|
||
return {
|
||
acp: {
|
||
enabled: true,
|
||
stream: {
|
||
deliveryMode: "live",
|
||
...streamOverrides,
|
||
},
|
||
},
|
||
} as Parameters<typeof createCfg>[0];
|
||
}
|
||
|
||
function createHiddenBoundaryCfg(
|
||
streamOverrides: Record<string, unknown> = {},
|
||
): Parameters<typeof createCfg>[0] {
|
||
return createLiveCfgOverrides({
|
||
coalesceIdleMs: 0,
|
||
maxChunkChars: 256,
|
||
...streamOverrides,
|
||
});
|
||
}
|
||
|
||
function blockDeliveries(deliveries: Delivery[]) {
|
||
return deliveries.filter((entry) => entry.kind === "block");
|
||
}
|
||
|
||
function combinedBlockText(deliveries: Delivery[]) {
|
||
return blockDeliveries(deliveries)
|
||
.map((entry) => entry.text ?? "")
|
||
.join("");
|
||
}
|
||
|
||
function expectToolCallSummary(delivery: Delivery | undefined) {
|
||
expect(delivery?.kind).toBe("tool");
|
||
expect(delivery?.text).toContain("Tool Call");
|
||
}
|
||
|
||
function createFinalOnlyStatusToolHarness() {
|
||
return createProjectorHarness({
|
||
acp: {
|
||
enabled: true,
|
||
stream: {
|
||
coalesceIdleMs: 0,
|
||
maxChunkChars: 512,
|
||
deliveryMode: "final_only",
|
||
tagVisibility: {
|
||
available_commands_update: true,
|
||
tool_call: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
function createLiveToolLifecycleHarness(params?: {
|
||
coalesceIdleMs?: number;
|
||
maxChunkChars?: number;
|
||
maxSessionUpdateChars?: number;
|
||
repeatSuppression?: boolean;
|
||
}) {
|
||
return createProjectorHarness({
|
||
acp: {
|
||
enabled: true,
|
||
stream: {
|
||
deliveryMode: "live",
|
||
...params,
|
||
tagVisibility: {
|
||
tool_call: true,
|
||
tool_call_update: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
function createLiveStatusAndToolLifecycleHarness(params?: {
|
||
coalesceIdleMs?: number;
|
||
maxChunkChars?: number;
|
||
repeatSuppression?: boolean;
|
||
}) {
|
||
return createProjectorHarness({
|
||
acp: {
|
||
enabled: true,
|
||
stream: {
|
||
deliveryMode: "live",
|
||
...params,
|
||
tagVisibility: {
|
||
available_commands_update: true,
|
||
tool_call: true,
|
||
tool_call_update: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
async function emitToolLifecycleEvent(
|
||
projector: ReturnType<typeof createProjectorHarness>["projector"],
|
||
event: {
|
||
tag: "tool_call" | "tool_call_update";
|
||
toolCallId: string;
|
||
status: "in_progress" | "completed";
|
||
title?: string;
|
||
text: string;
|
||
},
|
||
) {
|
||
await projector.onEvent({
|
||
type: "tool_call",
|
||
...event,
|
||
});
|
||
}
|
||
|
||
async function runHiddenBoundaryCase(params: {
|
||
cfgOverrides?: Parameters<typeof createCfg>[0];
|
||
toolCallId: string;
|
||
includeNonTerminalUpdate?: boolean;
|
||
firstText?: string;
|
||
secondText?: string;
|
||
expectedText: string;
|
||
}) {
|
||
const { deliveries, projector } = createProjectorHarness(params.cfgOverrides);
|
||
await projector.onEvent({
|
||
type: "text_delta",
|
||
text: params.firstText ?? "fallback.",
|
||
tag: "agent_message_chunk",
|
||
});
|
||
await projector.onEvent({
|
||
type: "tool_call",
|
||
tag: "tool_call",
|
||
toolCallId: params.toolCallId,
|
||
status: "in_progress",
|
||
title: "Run test",
|
||
text: "Run test (in_progress)",
|
||
});
|
||
if (params.includeNonTerminalUpdate) {
|
||
await projector.onEvent({
|
||
type: "tool_call",
|
||
tag: "tool_call_update",
|
||
toolCallId: params.toolCallId,
|
||
status: "in_progress",
|
||
title: "Run test",
|
||
text: "Run test (in_progress)",
|
||
});
|
||
}
|
||
await projector.onEvent({
|
||
type: "text_delta",
|
||
text: params.secondText ?? "I don't",
|
||
tag: "agent_message_chunk",
|
||
});
|
||
await projector.flush(true);
|
||
|
||
expect(combinedBlockText(deliveries)).toBe(params.expectedText);
|
||
}
|
||
|
||
describe("createAcpReplyProjector", () => {
|
||
it("coalesces text deltas into bounded block chunks", async () => {
|
||
const { deliveries, projector } = createProjectorHarness();
|
||
|
||
await projector.onEvent({
|
||
type: "text_delta",
|
||
text: "a".repeat(70),
|
||
tag: "agent_message_chunk",
|
||
});
|
||
await projector.flush(true);
|
||
|
||
expect(deliveries).toEqual([
|
||
{ kind: "block", text: "a".repeat(64) },
|
||
{ kind: "block", text: "a".repeat(6) },
|
||
]);
|
||
});
|
||
|
||
it("does not suppress identical short text across terminal turn boundaries", async () => {
|
||
const { deliveries, projector } = createProjectorHarness(
|
||
createLiveCfgOverrides({
|
||
coalesceIdleMs: 0,
|
||
maxChunkChars: 64,
|
||
}),
|
||
);
|
||
|
||
await projector.onEvent({ type: "text_delta", text: "A", tag: "agent_message_chunk" });
|
||
await projector.onEvent({ type: "done", stopReason: "end_turn" });
|
||
await projector.onEvent({ type: "text_delta", text: "A", tag: "agent_message_chunk" });
|
||
await projector.onEvent({ type: "done", stopReason: "end_turn" });
|
||
|
||
expect(blockDeliveries(deliveries)).toEqual([
|
||
{ kind: "block", text: "A" },
|
||
{ kind: "block", text: "A" },
|
||
]);
|
||
});
|
||
|
||
it("flushes staggered live text deltas after idle gaps", async () => {
|
||
vi.useFakeTimers();
|
||
try {
|
||
const { deliveries, projector } = createProjectorHarness(
|
||
createLiveCfgOverrides({
|
||
coalesceIdleMs: 50,
|
||
maxChunkChars: 64,
|
||
}),
|
||
);
|
||
|
||
await projector.onEvent({ type: "text_delta", text: "A", tag: "agent_message_chunk" });
|
||
await vi.advanceTimersByTimeAsync(760);
|
||
await projector.flush(false);
|
||
|
||
await projector.onEvent({ type: "text_delta", text: "B", tag: "agent_message_chunk" });
|
||
await vi.advanceTimersByTimeAsync(760);
|
||
await projector.flush(false);
|
||
|
||
await projector.onEvent({ type: "text_delta", text: "C", tag: "agent_message_chunk" });
|
||
await vi.advanceTimersByTimeAsync(760);
|
||
await projector.flush(false);
|
||
|
||
expect(blockDeliveries(deliveries)).toEqual([
|
||
{ kind: "block", text: "A" },
|
||
{ kind: "block", text: "B" },
|
||
{ kind: "block", text: "C" },
|
||
]);
|
||
} finally {
|
||
vi.useRealTimers();
|
||
}
|
||
});
|
||
|
||
it("splits oversized live text by maxChunkChars", async () => {
|
||
const { deliveries, projector } = createProjectorHarness({
|
||
acp: {
|
||
enabled: true,
|
||
stream: {
|
||
deliveryMode: "live",
|
||
coalesceIdleMs: 0,
|
||
maxChunkChars: 50,
|
||
},
|
||
},
|
||
});
|
||
|
||
const text = `${"a".repeat(50)}${"b".repeat(50)}${"c".repeat(20)}`;
|
||
await projector.onEvent({ type: "text_delta", text, tag: "agent_message_chunk" });
|
||
await projector.flush(true);
|
||
|
||
expect(blockDeliveries(deliveries)).toEqual([
|
||
{ kind: "block", text: "a".repeat(50) },
|
||
{ kind: "block", text: "b".repeat(50) },
|
||
{ kind: "block", text: "c".repeat(20) },
|
||
]);
|
||
});
|
||
|
||
it("does not flush short live fragments mid-phrase on idle", async () => {
|
||
vi.useFakeTimers();
|
||
try {
|
||
const { deliveries, projector } = createProjectorHarness(
|
||
createLiveCfgOverrides({
|
||
coalesceIdleMs: 100,
|
||
maxChunkChars: 256,
|
||
}),
|
||
);
|
||
|
||
await projector.onEvent({
|
||
type: "text_delta",
|
||
text: "Yes. Send me the term(s), and I’ll run ",
|
||
tag: "agent_message_chunk",
|
||
});
|
||
|
||
await vi.advanceTimersByTimeAsync(1200);
|
||
expect(deliveries).toEqual([]);
|
||
|
||
await projector.onEvent({
|
||
type: "text_delta",
|
||
text: "`wd-cli` searches right away. ",
|
||
tag: "agent_message_chunk",
|
||
});
|
||
await projector.flush(false);
|
||
|
||
expect(deliveries).toEqual([
|
||
{
|
||
kind: "block",
|
||
text: "Yes. Send me the term(s), and I’ll run `wd-cli` searches right away. ",
|
||
},
|
||
]);
|
||
} finally {
|
||
vi.useRealTimers();
|
||
}
|
||
});
|
||
|
||
it("supports deliveryMode=final_only by buffering all projected output until done", async () => {
|
||
const { deliveries, projector } = createFinalOnlyStatusToolHarness();
|
||
|
||
await projector.onEvent({
|
||
type: "text_delta",
|
||
text: "What",
|
||
tag: "agent_message_chunk",
|
||
});
|
||
await projector.onEvent({
|
||
type: "status",
|
||
text: "available commands updated (7)",
|
||
tag: "available_commands_update",
|
||
});
|
||
await projector.onEvent({
|
||
type: "tool_call",
|
||
tag: "tool_call",
|
||
toolCallId: "call_1",
|
||
status: "in_progress",
|
||
title: "List files",
|
||
text: "List files (in_progress)",
|
||
});
|
||
await projector.onEvent({
|
||
type: "text_delta",
|
||
text: " now?",
|
||
tag: "agent_message_chunk",
|
||
});
|
||
expect(deliveries).toEqual([]);
|
||
|
||
await projector.onEvent({ type: "done" });
|
||
expect(deliveries).toHaveLength(3);
|
||
expect(deliveries[0]).toEqual({
|
||
kind: "tool",
|
||
text: prefixSystemMessage("available commands updated (7)"),
|
||
});
|
||
expectToolCallSummary(deliveries[1]);
|
||
expect(deliveries[2]).toEqual({ kind: "block", text: "What now?" });
|
||
});
|
||
|
||
it("flushes buffered status/tool output on error in deliveryMode=final_only", async () => {
|
||
const { deliveries, projector } = createFinalOnlyStatusToolHarness();
|
||
|
||
await projector.onEvent({
|
||
type: "status",
|
||
text: "available commands updated (7)",
|
||
tag: "available_commands_update",
|
||
});
|
||
await projector.onEvent({
|
||
type: "tool_call",
|
||
tag: "tool_call",
|
||
toolCallId: "call_2",
|
||
status: "in_progress",
|
||
title: "Run tests",
|
||
text: "Run tests (in_progress)",
|
||
});
|
||
expect(deliveries).toEqual([]);
|
||
|
||
await projector.onEvent({ type: "error", message: "turn failed" });
|
||
expect(deliveries).toHaveLength(2);
|
||
expect(deliveries[0]).toEqual({
|
||
kind: "tool",
|
||
text: prefixSystemMessage("available commands updated (7)"),
|
||
});
|
||
expectToolCallSummary(deliveries[1]);
|
||
});
|
||
|
||
it("suppresses usage_update by default and allows deduped usage when tag-visible", async () => {
|
||
const { deliveries: hidden, projector: hiddenProjector } = createProjectorHarness();
|
||
await hiddenProjector.onEvent({
|
||
type: "status",
|
||
text: "usage updated: 10/100",
|
||
tag: "usage_update",
|
||
used: 10,
|
||
size: 100,
|
||
});
|
||
expect(hidden).toEqual([]);
|
||
|
||
const { deliveries: shown, projector: shownProjector } = createProjectorHarness(
|
||
createLiveCfgOverrides({
|
||
coalesceIdleMs: 0,
|
||
maxChunkChars: 64,
|
||
tagVisibility: {
|
||
usage_update: true,
|
||
},
|
||
}),
|
||
);
|
||
|
||
await shownProjector.onEvent({
|
||
type: "status",
|
||
text: "usage updated: 10/100",
|
||
tag: "usage_update",
|
||
used: 10,
|
||
size: 100,
|
||
});
|
||
await shownProjector.onEvent({
|
||
type: "status",
|
||
text: "usage updated: 10/100",
|
||
tag: "usage_update",
|
||
used: 10,
|
||
size: 100,
|
||
});
|
||
await shownProjector.onEvent({
|
||
type: "status",
|
||
text: "usage updated: 11/100",
|
||
tag: "usage_update",
|
||
used: 11,
|
||
size: 100,
|
||
});
|
||
|
||
expect(shown).toEqual([
|
||
{ kind: "tool", text: prefixSystemMessage("usage updated: 10/100") },
|
||
{ kind: "tool", text: prefixSystemMessage("usage updated: 11/100") },
|
||
]);
|
||
});
|
||
|
||
it("hides available_commands_update by default", async () => {
|
||
const { deliveries, projector } = createProjectorHarness();
|
||
await projector.onEvent({
|
||
type: "status",
|
||
text: "available commands updated (7)",
|
||
tag: "available_commands_update",
|
||
});
|
||
|
||
expect(deliveries).toEqual([]);
|
||
});
|
||
|
||
it("dedupes repeated tool lifecycle updates when repeatSuppression is enabled", async () => {
|
||
const { deliveries, projector } = createLiveToolLifecycleHarness();
|
||
|
||
await emitToolLifecycleEvent(projector, {
|
||
tag: "tool_call",
|
||
toolCallId: "call_1",
|
||
status: "in_progress",
|
||
title: "List files",
|
||
text: "List files (in_progress)",
|
||
});
|
||
await emitToolLifecycleEvent(projector, {
|
||
tag: "tool_call_update",
|
||
toolCallId: "call_1",
|
||
status: "in_progress",
|
||
title: "List files",
|
||
text: "List files (in_progress)",
|
||
});
|
||
await emitToolLifecycleEvent(projector, {
|
||
tag: "tool_call_update",
|
||
toolCallId: "call_1",
|
||
status: "completed",
|
||
title: "List files",
|
||
text: "List files (completed)",
|
||
});
|
||
await emitToolLifecycleEvent(projector, {
|
||
tag: "tool_call_update",
|
||
toolCallId: "call_1",
|
||
status: "completed",
|
||
title: "List files",
|
||
text: "List files (completed)",
|
||
});
|
||
|
||
expect(deliveries.length).toBe(2);
|
||
expectToolCallSummary(deliveries[0]);
|
||
expectToolCallSummary(deliveries[1]);
|
||
});
|
||
|
||
it("keeps terminal tool updates even when rendered summaries are truncated", async () => {
|
||
const { deliveries, projector } = createLiveToolLifecycleHarness({
|
||
maxSessionUpdateChars: 48,
|
||
});
|
||
|
||
const longTitle =
|
||
"Run an intentionally long command title that truncates before lifecycle status is visible";
|
||
await emitToolLifecycleEvent(projector, {
|
||
tag: "tool_call",
|
||
toolCallId: "call_truncated_status",
|
||
status: "in_progress",
|
||
title: longTitle,
|
||
text: `${longTitle} (in_progress)`,
|
||
});
|
||
await emitToolLifecycleEvent(projector, {
|
||
tag: "tool_call_update",
|
||
toolCallId: "call_truncated_status",
|
||
status: "completed",
|
||
title: longTitle,
|
||
text: `${longTitle} (completed)`,
|
||
});
|
||
|
||
expect(deliveries.length).toBe(2);
|
||
expectToolCallSummary(deliveries[0]);
|
||
expectToolCallSummary(deliveries[1]);
|
||
});
|
||
|
||
it("renders fallback tool labels without leaking call ids as primary label", async () => {
|
||
const { deliveries, projector } = createLiveToolLifecycleHarness();
|
||
|
||
await projector.onEvent({
|
||
type: "tool_call",
|
||
tag: "tool_call",
|
||
toolCallId: "call_ABC123",
|
||
status: "in_progress",
|
||
text: "call_ABC123 (in_progress)",
|
||
});
|
||
|
||
expectToolCallSummary(deliveries[0]);
|
||
expect(deliveries[0]?.text).not.toContain("call_ABC123 (");
|
||
});
|
||
|
||
it("allows repeated status/tool summaries when repeatSuppression is disabled", async () => {
|
||
const { deliveries, projector } = createLiveStatusAndToolLifecycleHarness({
|
||
coalesceIdleMs: 0,
|
||
maxChunkChars: 256,
|
||
repeatSuppression: false,
|
||
});
|
||
|
||
await projector.onEvent({
|
||
type: "status",
|
||
text: "available commands updated",
|
||
tag: "available_commands_update",
|
||
});
|
||
await projector.onEvent({
|
||
type: "status",
|
||
text: "available commands updated",
|
||
tag: "available_commands_update",
|
||
});
|
||
await projector.onEvent({
|
||
type: "tool_call",
|
||
text: "tool call",
|
||
tag: "tool_call",
|
||
toolCallId: "x",
|
||
status: "in_progress",
|
||
});
|
||
await projector.onEvent({
|
||
type: "tool_call",
|
||
text: "tool call",
|
||
tag: "tool_call_update",
|
||
toolCallId: "x",
|
||
status: "in_progress",
|
||
});
|
||
await projector.onEvent({
|
||
type: "text_delta",
|
||
text: "hello",
|
||
tag: "agent_message_chunk",
|
||
});
|
||
await projector.flush(true);
|
||
|
||
expect(deliveries.filter((entry) => entry.kind === "tool").length).toBe(4);
|
||
expect(deliveries[0]).toEqual({
|
||
kind: "tool",
|
||
text: prefixSystemMessage("available commands updated"),
|
||
});
|
||
expect(deliveries[1]).toEqual({
|
||
kind: "tool",
|
||
text: prefixSystemMessage("available commands updated"),
|
||
});
|
||
expectToolCallSummary(deliveries[2]);
|
||
expectToolCallSummary(deliveries[3]);
|
||
expect(deliveries[4]).toEqual({ kind: "block", text: "hello" });
|
||
});
|
||
|
||
it("suppresses exact duplicate status updates when repeatSuppression is enabled", async () => {
|
||
const { deliveries, projector } = createProjectorHarness(
|
||
createLiveCfgOverrides({
|
||
coalesceIdleMs: 0,
|
||
maxChunkChars: 256,
|
||
tagVisibility: {
|
||
available_commands_update: true,
|
||
},
|
||
}),
|
||
);
|
||
|
||
await projector.onEvent({
|
||
type: "status",
|
||
text: "available commands updated (7)",
|
||
tag: "available_commands_update",
|
||
});
|
||
await projector.onEvent({
|
||
type: "status",
|
||
text: "available commands updated (7)",
|
||
tag: "available_commands_update",
|
||
});
|
||
await projector.onEvent({
|
||
type: "status",
|
||
text: "available commands updated (8)",
|
||
tag: "available_commands_update",
|
||
});
|
||
|
||
expect(deliveries).toEqual([
|
||
{ kind: "tool", text: prefixSystemMessage("available commands updated (7)") },
|
||
{ kind: "tool", text: prefixSystemMessage("available commands updated (8)") },
|
||
]);
|
||
});
|
||
|
||
it("truncates oversized turns once and emits one truncation notice", async () => {
|
||
const { deliveries, projector } = createProjectorHarness({
|
||
acp: {
|
||
enabled: true,
|
||
stream: {
|
||
coalesceIdleMs: 0,
|
||
maxChunkChars: 256,
|
||
deliveryMode: "live",
|
||
maxOutputChars: 5,
|
||
},
|
||
},
|
||
});
|
||
|
||
await projector.onEvent({
|
||
type: "text_delta",
|
||
text: "hello world",
|
||
tag: "agent_message_chunk",
|
||
});
|
||
await projector.onEvent({
|
||
type: "text_delta",
|
||
text: "ignored tail",
|
||
tag: "agent_message_chunk",
|
||
});
|
||
await projector.flush(true);
|
||
|
||
expect(deliveries).toHaveLength(2);
|
||
expect(deliveries).toContainEqual({ kind: "block", text: "hello" });
|
||
expect(deliveries).toContainEqual({
|
||
kind: "tool",
|
||
text: prefixSystemMessage("output truncated"),
|
||
});
|
||
});
|
||
|
||
it("supports tagVisibility overrides for tool updates", async () => {
|
||
const { deliveries, projector } = createProjectorHarness({
|
||
acp: {
|
||
enabled: true,
|
||
stream: {
|
||
coalesceIdleMs: 0,
|
||
maxChunkChars: 256,
|
||
deliveryMode: "live",
|
||
tagVisibility: {
|
||
tool_call: true,
|
||
tool_call_update: false,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
await projector.onEvent({
|
||
type: "tool_call",
|
||
tag: "tool_call",
|
||
toolCallId: "c1",
|
||
status: "in_progress",
|
||
title: "Run tests",
|
||
text: "Run tests (in_progress)",
|
||
});
|
||
await projector.onEvent({
|
||
type: "tool_call",
|
||
tag: "tool_call_update",
|
||
toolCallId: "c1",
|
||
status: "completed",
|
||
title: "Run tests",
|
||
text: "Run tests (completed)",
|
||
});
|
||
|
||
expect(deliveries.length).toBe(1);
|
||
expectToolCallSummary(deliveries[0]);
|
||
});
|
||
|
||
it("inserts a space boundary before visible text after hidden tool updates by default", async () => {
|
||
await runHiddenBoundaryCase({
|
||
cfgOverrides: createHiddenBoundaryCfg(),
|
||
toolCallId: "call_hidden_1",
|
||
expectedText: "fallback. I don't",
|
||
});
|
||
});
|
||
|
||
it("preserves hidden boundary across nonterminal hidden tool updates", async () => {
|
||
await runHiddenBoundaryCase({
|
||
cfgOverrides: createHiddenBoundaryCfg({
|
||
tagVisibility: {
|
||
tool_call: false,
|
||
tool_call_update: false,
|
||
},
|
||
}),
|
||
toolCallId: "hidden_boundary_1",
|
||
includeNonTerminalUpdate: true,
|
||
expectedText: "fallback. I don't",
|
||
});
|
||
});
|
||
|
||
it("supports hiddenBoundarySeparator=space", async () => {
|
||
await runHiddenBoundaryCase({
|
||
cfgOverrides: createHiddenBoundaryCfg({
|
||
hiddenBoundarySeparator: "space",
|
||
}),
|
||
toolCallId: "call_hidden_2",
|
||
expectedText: "fallback. I don't",
|
||
});
|
||
});
|
||
|
||
it("supports hiddenBoundarySeparator=none", async () => {
|
||
await runHiddenBoundaryCase({
|
||
cfgOverrides: createHiddenBoundaryCfg({
|
||
hiddenBoundarySeparator: "none",
|
||
}),
|
||
toolCallId: "call_hidden_3",
|
||
expectedText: "fallback.I don't",
|
||
});
|
||
});
|
||
|
||
it("does not duplicate newlines when previous visible text already ends with newline", async () => {
|
||
await runHiddenBoundaryCase({
|
||
cfgOverrides: createHiddenBoundaryCfg(),
|
||
toolCallId: "call_hidden_4",
|
||
firstText: "fallback.\n",
|
||
expectedText: "fallback.\nI don't",
|
||
});
|
||
});
|
||
|
||
it("does not insert boundary separator for hidden non-tool status updates", async () => {
|
||
const { deliveries, projector } = createProjectorHarness({
|
||
acp: {
|
||
enabled: true,
|
||
stream: {
|
||
coalesceIdleMs: 0,
|
||
maxChunkChars: 256,
|
||
deliveryMode: "live",
|
||
},
|
||
},
|
||
});
|
||
|
||
await projector.onEvent({ type: "text_delta", text: "A", tag: "agent_message_chunk" });
|
||
await projector.onEvent({
|
||
type: "status",
|
||
tag: "available_commands_update",
|
||
text: "available commands updated",
|
||
});
|
||
await projector.onEvent({ type: "text_delta", text: "B", tag: "agent_message_chunk" });
|
||
await projector.flush(true);
|
||
|
||
expect(combinedBlockText(deliveries)).toBe("AB");
|
||
});
|
||
});
|