evOo-local/packages/adapters/src/user-progress.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

105 lines
3.9 KiB
TypeScript

import type { MessageBlock } from "@rakazo/contracts";
import { isToolActivityBlock } from "@rakazo/core";
import { describe, expect, it } from "vitest";
import {
botMessageOutcomeFromMidTurn,
clampUserProgressMessage,
extractNarrationText,
finalBlocksAfterMidTurnProgress,
isProgressMessageTruncated,
isUserProgressClientNonce,
USER_PROGRESS_MESSAGE_MAX_LENGTH,
userProgressClientNonce,
} from "./user-progress.js";
describe("clampUserProgressMessage", () => {
it("trims and rejects empty text", () => {
expect(clampUserProgressMessage(" hello ")).toBe("hello");
expect(clampUserProgressMessage(" ")).toBe("");
});
it("clamps long progress beats", () => {
const clamped = clampUserProgressMessage("x".repeat(USER_PROGRESS_MESSAGE_MAX_LENGTH + 40));
expect(clamped).toHaveLength(USER_PROGRESS_MESSAGE_MAX_LENGTH);
expect(clamped.endsWith("…")).toBe(true);
});
});
describe("isProgressMessageTruncated", () => {
it("is false for text that fits", () => {
expect(isProgressMessageTruncated("short update")).toBe(false);
expect(isProgressMessageTruncated("x".repeat(USER_PROGRESS_MESSAGE_MAX_LENGTH))).toBe(false);
});
it("is true for text that would be cut off by clampUserProgressMessage", () => {
const long = "x".repeat(USER_PROGRESS_MESSAGE_MAX_LENGTH + 1);
expect(isProgressMessageTruncated(long)).toBe(true);
expect(clampUserProgressMessage(long).endsWith("…")).toBe(true);
});
});
describe("extractNarrationText", () => {
it("pulls text blocks and current text while keeping tool activity", () => {
const { text, remaining } = extractNarrationText(
[
{ kind: "text", text: "Checking calendars. " },
{ kind: "steps", steps: [{ label: "Web search", count: 1 }] },
{ kind: "text", text: "Found three options." },
],
" Still looking.",
);
expect(text).toBe("Checking calendars. Found three options. Still looking.");
expect(remaining).toEqual([{ kind: "steps", steps: [{ label: "Web search", count: 1 }] }]);
});
});
describe("finalBlocksAfterMidTurnProgress", () => {
it("drops a hollow final message that is only hidden tool activity", () => {
const steps: MessageBlock = { kind: "steps", steps: [{ label: "Shell", count: 2 }] };
expect(finalBlocksAfterMidTurnProgress([steps], true)).toEqual([]);
expect(finalBlocksAfterMidTurnProgress([steps], false)).toEqual([steps]);
});
it("keeps a final answer alongside tool activity", () => {
const blocks: MessageBlock[] = [
{ kind: "steps", steps: [{ label: "Web search", count: 1 }] },
{ kind: "text", text: "You are free Tuesday afternoon." },
];
expect(finalBlocksAfterMidTurnProgress(blocks, true)).toEqual(blocks);
expect(isToolActivityBlock(blocks[0]!)).toBe(true);
});
});
describe("botMessageOutcomeFromMidTurn", () => {
it("prefers the final reply as a result", () => {
expect(botMessageOutcomeFromMidTurn("All set.", ["Checking calendars…"])).toEqual({
text: "All set.",
intent: "result",
});
});
it("returns mid-turn progress as status when there is no final reply", () => {
expect(
botMessageOutcomeFromMidTurn("", ["Checking calendars…", "Found three free slots."]),
).toEqual({
text: "Checking calendars…\n\nFound three free slots.",
intent: "status",
});
});
it("returns null when nothing was posted", () => {
expect(botMessageOutcomeFromMidTurn(" ", [])).toBeNull();
});
});
describe("userProgressClientNonce", () => {
it("tags mid-turn progress messages for reconciler detection", () => {
const nonce = userProgressClientNonce("run-1", 0);
expect(nonce.startsWith("user-progress:run-1:0:")).toBe(true);
expect(isUserProgressClientNonce(nonce)).toBe(true);
expect(userProgressClientNonce("run-1", 0)).not.toBe(nonce);
expect(isUserProgressClientNonce(null)).toBe(false);
expect(isUserProgressClientNonce("other")).toBe(false);
});
});