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" );
});
});
Messung V0.5 in Prozent C=99 H=97 G=97
¤ Dauer der Verarbeitung: 0.14 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland