Files
openwork/evals/specs/renderer-bounded-caches.test.ts

116 lines
5.9 KiB
TypeScript

import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { expect } from "vitest";
import { test } from "@openwork/testkit";
import type { ProviderListResponse } from "@opencode-ai/sdk/v2/client";
import type { Client } from "../../apps/app/src/app/types";
import {
fetchProviderList,
getConnectedProviderSnapshotChange,
} from "../../apps/app/src/react-app/infra/provider-list-query";
import {
MAX_TRACKED_MESSAGE_ROLES,
useSessionActivityStore,
} from "../../apps/app/src/react-app/domains/session/status/session-activity-store";
import {
appendVoiceTimelineEntry,
getVoiceRuntimeSnapshot,
initialVoiceRuntimeSnapshot,
resetVoiceRuntimeSnapshot,
VOICE_TIMELINE_LIMIT,
} from "../../apps/app/src/react-app/domains/session/voice/voice-runtime";
test("session activity message-role tracking stays bounded per session", () => {
const store = useSessionActivityStore.getState();
store.setRunStatus("ws-bounded", "session-a", "busy");
for (let index = 0; index < MAX_TRACKED_MESSAGE_ROLES + 50; index += 1) {
store.markMessageRole("ws-bounded", "session-a", `message-${index}`, index % 2 === 0 ? "user" : "assistant");
}
const record = useSessionActivityStore.getState().recordsByWorkspaceId["ws-bounded"]?.["session-a"];
expect(record).toBeDefined();
// The dict is capped at the most recent marks: the oldest overflowed
// message ids are dropped, the newest are retained with their roles.
expect(Object.keys(record?.messageRoles ?? {})).toHaveLength(MAX_TRACKED_MESSAGE_ROLES);
expect(record?.messageRoles["message-0"]).toBeUndefined();
expect(record?.messageRoles["message-49"]).toBeUndefined();
expect(record?.messageRoles["message-50"]).toBe("user");
expect(record?.messageRoles[`message-${MAX_TRACKED_MESSAGE_ROLES + 49}`]).toBe("assistant");
// Assistant-output gating still works for a retained assistant message.
store.markAssistantOutput("ws-bounded", "session-a", `message-${MAX_TRACKED_MESSAGE_ROLES + 49}`);
expect(
useSessionActivityStore.getState().recordsByWorkspaceId["ws-bounded"]?.["session-a"]?.assistantOutput,
).toBe(true);
// Negative half: below the cap nothing is evicted.
store.setRunStatus("ws-small", "session-b", "busy");
for (let index = 0; index < 10; index += 1) {
store.markMessageRole("ws-small", "session-b", `small-${index}`, "assistant");
}
const smallRecord = useSessionActivityStore.getState().recordsByWorkspaceId["ws-small"]?.["session-b"];
expect(Object.keys(smallRecord?.messageRoles ?? {})).toHaveLength(10);
expect(smallRecord?.messageRoles["small-0"]).toBe("assistant");
});
test("connected provider snapshot cache evicts the oldest workspace keys", async () => {
const emptyProviderList: ProviderListResponse = { all: [], connected: [], default: {} };
// Only provider.list is exercised by fetchProviderList; a full Client cannot
// be constructed in a unit spec.
const client = {
provider: { list: async () => ({ data: emptyProviderList }) },
} as unknown as Client;
for (let index = 0; index < 20; index += 1) {
await fetchProviderList({ client, baseUrl: "http://localhost:1", directory: `/tmp/workspace-${index}` });
}
// 20 distinct (baseUrl, directory) keys were recorded against a cap of 16:
// the oldest keys are gone, the newest are retained.
expect(getConnectedProviderSnapshotChange({ baseUrl: "http://localhost:1", directory: "/tmp/workspace-0" })).toBeNull();
expect(getConnectedProviderSnapshotChange({ baseUrl: "http://localhost:1", directory: "/tmp/workspace-3" })).toBeNull();
expect(getConnectedProviderSnapshotChange({ baseUrl: "http://localhost:1", directory: "/tmp/workspace-4" })).not.toBeNull();
expect(getConnectedProviderSnapshotChange({ baseUrl: "http://localhost:1", directory: "/tmp/workspace-19" })).not.toBeNull();
// Re-recording an existing key refreshes its recency instead of
// double-counting it: nothing else is evicted.
await fetchProviderList({ client, baseUrl: "http://localhost:1", directory: "/tmp/workspace-4" });
expect(getConnectedProviderSnapshotChange({ baseUrl: "http://localhost:1", directory: "/tmp/workspace-4" })).not.toBeNull();
expect(getConnectedProviderSnapshotChange({ baseUrl: "http://localhost:1", directory: "/tmp/workspace-5" })).not.toBeNull();
});
test("voice runtime timeline stays bounded and an explicit stop releases the snapshot", () => {
resetVoiceRuntimeSnapshot();
for (let index = 0; index < VOICE_TIMELINE_LIMIT + 30; index += 1) {
appendVoiceTimelineEntry("assistant", `spoken line ${index}`);
}
const bounded = getVoiceRuntimeSnapshot();
expect(bounded.entries).toHaveLength(VOICE_TIMELINE_LIMIT);
expect(bounded.entries[0]?.text).toBe("spoken line 30");
expect(bounded.entries.at(-1)?.text).toBe(`spoken line ${VOICE_TIMELINE_LIMIT + 29}`);
// Existing filtering behavior is preserved: blank user/assistant text is
// dropped, tool entries fall back to the tool name.
appendVoiceTimelineEntry("user", " ");
expect(getVoiceRuntimeSnapshot().entries).toHaveLength(VOICE_TIMELINE_LIMIT);
appendVoiceTimelineEntry("tool", "", { toolName: "openwork_snapshot" });
expect(getVoiceRuntimeSnapshot().entries.at(-1)?.text).toBe("openwork_snapshot");
// Reset drops every retained entry and transcript, returning the exact
// initial snapshot object shape.
resetVoiceRuntimeSnapshot();
expect(getVoiceRuntimeSnapshot()).toEqual(initialVoiceRuntimeSnapshot);
expect(getVoiceRuntimeSnapshot().entries).toHaveLength(0);
// The panel's explicit-stop path (non-silent disconnect) is wired to the
// reset, while the silent reconnect path keeps the timeline.
const voicePanelSource = readFileSync(
fileURLToPath(new URL("../../apps/app/src/react-app/domains/session/voice/voice-panel.tsx", import.meta.url)),
"utf8",
);
const stopBranch = voicePanelSource.match(/if \(silent\) \{[\s\S]*?\} else \{([\s\S]*?)\n {4}\}/)?.[1] ?? "";
expect(stopBranch).toContain("resetVoiceRuntimeSnapshot()");
});