First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,665 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { AgentState } from "@/features/agents/state/store";
|
||||
import { sendChatMessageViaStudio } from "@/features/agents/operations/chatSendOperation";
|
||||
import { GatewayResponseError } from "@/lib/gateway/errors";
|
||||
import { formatMetaMarkdown } from "@/lib/text/message-extract";
|
||||
|
||||
const createAgent = (overrides?: Partial<AgentState>): AgentState => {
|
||||
const base: AgentState = {
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:studio:test-session",
|
||||
status: "idle",
|
||||
sessionCreated: false,
|
||||
awaitingUserInput: false,
|
||||
hasUnseenActivity: false,
|
||||
outputLines: [],
|
||||
lastResult: null,
|
||||
lastDiff: null,
|
||||
runId: null,
|
||||
runStartedAt: null,
|
||||
streamText: null,
|
||||
thinkingTrace: null,
|
||||
latestOverride: null,
|
||||
latestOverrideKind: null,
|
||||
lastAssistantMessageAt: null,
|
||||
lastActivityAt: null,
|
||||
latestPreview: null,
|
||||
lastUserMessage: null,
|
||||
draft: "",
|
||||
sessionSettingsSynced: true,
|
||||
historyLoadedAt: null,
|
||||
historyFetchLimit: null,
|
||||
historyFetchedCount: null,
|
||||
historyMaybeTruncated: false,
|
||||
toolCallingEnabled: true,
|
||||
showThinkingTraces: true,
|
||||
model: "openai/gpt-5",
|
||||
thinkingLevel: "medium",
|
||||
avatarSeed: "seed-1",
|
||||
avatarUrl: null,
|
||||
};
|
||||
const merged = { ...base, ...(overrides ?? {}) };
|
||||
|
||||
return {
|
||||
...merged,
|
||||
historyFetchLimit: merged.historyFetchLimit ?? null,
|
||||
historyFetchedCount: merged.historyFetchedCount ?? null,
|
||||
historyMaybeTruncated: merged.historyMaybeTruncated ?? false,
|
||||
};
|
||||
};
|
||||
|
||||
const createWebchatBlockedPatchError = () =>
|
||||
new GatewayResponseError({
|
||||
code: "INVALID_REQUEST",
|
||||
message: "webchat clients cannot patch sessions; use chat.send for session-scoped updates",
|
||||
});
|
||||
|
||||
describe("sendChatMessageViaStudio", () => {
|
||||
it("handles_reset_command", async () => {
|
||||
const agent = createAgent({
|
||||
outputLines: ["old"],
|
||||
streamText: "stream",
|
||||
thinkingTrace: "thinking",
|
||||
lastResult: "result",
|
||||
sessionSettingsSynced: true,
|
||||
});
|
||||
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async () => ({}));
|
||||
const clearRunTracking = vi.fn();
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "/reset",
|
||||
clearRunTracking,
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
expect(clearRunTracking).toHaveBeenCalledWith("run-1");
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "updateAgent",
|
||||
agentId: agent.agentId,
|
||||
patch: expect.objectContaining({
|
||||
outputLines: [],
|
||||
streamText: null,
|
||||
thinkingTrace: null,
|
||||
lastResult: null,
|
||||
transcriptEntries: [],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("syncs_session_settings_when_not_synced", async () => {
|
||||
const agent = createAgent({ sessionSettingsSynced: false, sessionCreated: false });
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async (method: string) => {
|
||||
if (method === "sessions.patch") {
|
||||
return {
|
||||
ok: true,
|
||||
key: agent.sessionKey,
|
||||
entry: { thinkingLevel: "medium" },
|
||||
resolved: { modelProvider: "openai", model: "gpt-5" },
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "hello",
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
const methods = call.mock.calls.map((entry) => entry[0]);
|
||||
expect(methods).toEqual(["sessions.patch", "chat.send"]);
|
||||
expect(call).toHaveBeenCalledWith(
|
||||
"sessions.patch",
|
||||
expect.objectContaining({
|
||||
key: agent.sessionKey,
|
||||
model: "openai/gpt-5",
|
||||
thinkingLevel: "medium",
|
||||
})
|
||||
);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: "updateAgent",
|
||||
agentId: agent.agentId,
|
||||
patch: { sessionSettingsSynced: true, sessionCreated: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("continues_send_when_webchat_patch_is_blocked", async () => {
|
||||
const agent = createAgent({ sessionSettingsSynced: false, sessionCreated: false });
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async (method: string, payload?: unknown) => {
|
||||
if (method === "sessions.patch") {
|
||||
throw createWebchatBlockedPatchError();
|
||||
}
|
||||
if (method === "chat.send") {
|
||||
const runId =
|
||||
payload &&
|
||||
typeof payload === "object" &&
|
||||
"idempotencyKey" in payload &&
|
||||
typeof payload.idempotencyKey === "string"
|
||||
? payload.idempotencyKey
|
||||
: "run";
|
||||
return { runId, status: "started" };
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "hello",
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
const methods = call.mock.calls.map((entry) => entry[0]);
|
||||
expect(methods).toEqual(["sessions.patch", "chat.send"]);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: "updateAgent",
|
||||
agentId: agent.agentId,
|
||||
patch: { sessionSettingsSynced: true, sessionCreated: true },
|
||||
});
|
||||
|
||||
const errorLines = dispatch.mock.calls
|
||||
.map((entry) => entry[0])
|
||||
.filter(
|
||||
(
|
||||
action
|
||||
): action is {
|
||||
type: "appendOutput";
|
||||
line: string;
|
||||
} =>
|
||||
action &&
|
||||
typeof action === "object" &&
|
||||
"type" in action &&
|
||||
action.type === "appendOutput" &&
|
||||
"line" in action &&
|
||||
typeof action.line === "string" &&
|
||||
action.line.startsWith("Error:")
|
||||
)
|
||||
.map((action) => action.line);
|
||||
expect(errorLines).toEqual([]);
|
||||
});
|
||||
|
||||
it("fails_send_when_patch_error_is_not_webchat_blocked", async () => {
|
||||
const agent = createAgent({ sessionSettingsSynced: false, sessionCreated: false });
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async (method: string) => {
|
||||
if (method === "sessions.patch") {
|
||||
throw new GatewayResponseError({
|
||||
code: "INVALID_REQUEST",
|
||||
message: "invalid model ref",
|
||||
});
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "hello",
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
const methods = call.mock.calls.map((entry) => entry[0]);
|
||||
expect(methods).toEqual(["sessions.patch"]);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: "appendOutput",
|
||||
agentId: agent.agentId,
|
||||
line: "Error: invalid model ref",
|
||||
});
|
||||
});
|
||||
|
||||
it("suppresses_patch_retry_after_webchat_blocked_patch_error", async () => {
|
||||
let agent = createAgent({ sessionSettingsSynced: false, sessionCreated: false });
|
||||
const dispatch = vi.fn(
|
||||
(action: { type: string; agentId?: string; patch?: Partial<AgentState> }) => {
|
||||
if (action.type !== "updateAgent" || action.agentId !== agent.agentId || !action.patch) {
|
||||
return;
|
||||
}
|
||||
agent = { ...agent, ...action.patch };
|
||||
}
|
||||
);
|
||||
const call = vi.fn(async (method: string, payload?: unknown) => {
|
||||
if (method === "sessions.patch") {
|
||||
throw createWebchatBlockedPatchError();
|
||||
}
|
||||
if (method === "chat.send") {
|
||||
const runId =
|
||||
payload &&
|
||||
typeof payload === "object" &&
|
||||
"idempotencyKey" in payload &&
|
||||
typeof payload.idempotencyKey === "string"
|
||||
? payload.idempotencyKey
|
||||
: "run";
|
||||
return { runId, status: "started" };
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "first",
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "second",
|
||||
now: () => 1240,
|
||||
generateRunId: () => "run-2",
|
||||
});
|
||||
|
||||
const methods = call.mock.calls.map((entry) => entry[0]);
|
||||
expect(methods.filter((method) => method === "sessions.patch")).toHaveLength(1);
|
||||
expect(methods.filter((method) => method === "chat.send")).toHaveLength(2);
|
||||
expect(agent.sessionSettingsSynced).toBe(true);
|
||||
expect(agent.sessionCreated).toBe(true);
|
||||
});
|
||||
|
||||
it("syncs exec session overrides for ask-first agents", async () => {
|
||||
const agent = createAgent({
|
||||
sessionSettingsSynced: false,
|
||||
sessionCreated: false,
|
||||
sessionExecHost: "gateway",
|
||||
sessionExecSecurity: "allowlist",
|
||||
sessionExecAsk: "always",
|
||||
});
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async (method: string) => {
|
||||
if (method === "sessions.patch") {
|
||||
return {
|
||||
ok: true,
|
||||
key: agent.sessionKey,
|
||||
entry: { thinkingLevel: "medium" },
|
||||
resolved: { modelProvider: "openai", model: "gpt-5" },
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "hello",
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
expect(call).toHaveBeenCalledWith(
|
||||
"sessions.patch",
|
||||
expect.objectContaining({
|
||||
key: agent.sessionKey,
|
||||
execHost: "gateway",
|
||||
execSecurity: "allowlist",
|
||||
execAsk: "always",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does_not_sync_session_settings_when_already_synced", async () => {
|
||||
const agent = createAgent({ sessionSettingsSynced: true });
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async () => ({ runId: "run-1", status: "started" }));
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "hello",
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
expect(call).toHaveBeenCalledWith(
|
||||
"chat.send",
|
||||
expect.objectContaining({ sessionKey: agent.sessionKey })
|
||||
);
|
||||
expect(call).not.toHaveBeenCalledWith(
|
||||
"sessions.patch",
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it("clears_running_state_for_unknown_success_payload_shape", async () => {
|
||||
const agent = createAgent({ sessionSettingsSynced: true, sessionCreated: true });
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async () => ({ ok: true }));
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "hello",
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
const idlePatchAction = dispatch.mock.calls
|
||||
.map((entry) => entry[0])
|
||||
.find(
|
||||
(action) =>
|
||||
action?.type === "updateAgent" &&
|
||||
action?.agentId === agent.agentId &&
|
||||
action?.patch?.status === "idle" &&
|
||||
action?.patch?.runId === null
|
||||
);
|
||||
expect(idlePatchAction).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clears_running_state_for_stop_style_immediate_success_payload", async () => {
|
||||
const agent = createAgent({ sessionSettingsSynced: true, sessionCreated: true });
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async () => ({ ok: true, aborted: false, runIds: [] }));
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "stop please",
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
const idlePatchAction = dispatch.mock.calls
|
||||
.map((entry) => entry[0])
|
||||
.find(
|
||||
(action) =>
|
||||
action?.type === "updateAgent" &&
|
||||
action?.agentId === agent.agentId &&
|
||||
action?.patch?.status === "idle" &&
|
||||
action?.patch?.runId === null &&
|
||||
action?.patch?.runStartedAt === null &&
|
||||
action?.patch?.streamText === null &&
|
||||
action?.patch?.thinkingTrace === null
|
||||
);
|
||||
expect(idlePatchAction).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps_running_state_for_matching_streaming_status_payloads", async () => {
|
||||
const payloads = [{ runId: "run-1", status: "started" }, { runId: "run-1", status: "in_flight" }];
|
||||
|
||||
for (const payload of payloads) {
|
||||
const agent = createAgent({ sessionSettingsSynced: true, sessionCreated: true });
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async () => payload);
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "hello",
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
const idlePatchAction = dispatch.mock.calls
|
||||
.map((entry) => entry[0])
|
||||
.find(
|
||||
(action) =>
|
||||
action?.type === "updateAgent" &&
|
||||
action?.agentId === agent.agentId &&
|
||||
action?.patch?.status === "idle"
|
||||
);
|
||||
expect(idlePatchAction).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("clears_running_state_for_streaming_shape_with_mismatched_run_id", async () => {
|
||||
const agent = createAgent({ sessionSettingsSynced: true, sessionCreated: true });
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async () => ({ runId: "different-run", status: "started" }));
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "hello",
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
const idlePatchAction = dispatch.mock.calls
|
||||
.map((entry) => entry[0])
|
||||
.find(
|
||||
(action) =>
|
||||
action?.type === "updateAgent" &&
|
||||
action?.agentId === agent.agentId &&
|
||||
action?.patch?.status === "idle" &&
|
||||
action?.patch?.runId === null
|
||||
);
|
||||
expect(idlePatchAction).toBeTruthy();
|
||||
});
|
||||
|
||||
it("supports_internal_send_without_local_user_echo", async () => {
|
||||
const agent = createAgent({ sessionSettingsSynced: true });
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async () => ({ ok: true }));
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "internal follow-up",
|
||||
echoUserMessage: false,
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
const dispatchedActions = dispatch.mock.calls.map((entry) => entry[0]);
|
||||
expect(
|
||||
dispatchedActions.some(
|
||||
(action) => action.type === "appendOutput" && action.line === "> internal follow-up"
|
||||
)
|
||||
).toBe(false);
|
||||
const runningUpdate = dispatchedActions.find(
|
||||
(action) => action.type === "updateAgent" && action.patch?.status === "running"
|
||||
);
|
||||
expect(runningUpdate).toBeTruthy();
|
||||
if (runningUpdate && runningUpdate.type === "updateAgent") {
|
||||
expect(runningUpdate.patch.lastUserMessage).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("marks_error_on_gateway_failure", async () => {
|
||||
const agent = createAgent({ sessionSettingsSynced: true });
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async (method: string) => {
|
||||
if (method === "chat.send") {
|
||||
throw new Error("boom");
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "hello",
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: "updateAgent",
|
||||
agentId: agent.agentId,
|
||||
patch: { status: "error", runId: null, runStartedAt: null, streamText: null, thinkingTrace: null },
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: "appendOutput",
|
||||
agentId: agent.agentId,
|
||||
line: "Error: boom",
|
||||
});
|
||||
});
|
||||
|
||||
it("optimistically_appends_only_user_content_line", async () => {
|
||||
const agent = createAgent({ sessionSettingsSynced: true });
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async () => ({ ok: true }));
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "Hello world",
|
||||
now: () => 1234,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
const appendLines = dispatch.mock.calls
|
||||
.map((entry) => entry[0])
|
||||
.filter((action): action is { type: "appendOutput"; line: string } => {
|
||||
return Boolean(
|
||||
action &&
|
||||
typeof action === "object" &&
|
||||
"type" in action &&
|
||||
action.type === "appendOutput" &&
|
||||
"line" in action &&
|
||||
typeof action.line === "string"
|
||||
);
|
||||
})
|
||||
.map((action) => action.line);
|
||||
|
||||
expect(appendLines).toContain("> Hello world");
|
||||
expect(appendLines.some((line) => line.startsWith("[[meta]]"))).toBe(false);
|
||||
});
|
||||
|
||||
it("uses_monotonic_timestamp_for_optimistic_user_turn_ordering", async () => {
|
||||
const sessionKey = "agent:agent-1:studio:test-session";
|
||||
const agent = createAgent({
|
||||
sessionSettingsSynced: true,
|
||||
transcriptEntries: [
|
||||
{
|
||||
entryId: "history:assistant:1",
|
||||
role: "assistant",
|
||||
kind: "assistant",
|
||||
text: "previous assistant",
|
||||
sessionKey,
|
||||
runId: null,
|
||||
source: "history",
|
||||
timestampMs: 5000,
|
||||
sequenceKey: 10,
|
||||
confirmed: true,
|
||||
fingerprint: "fp-prev-assistant",
|
||||
},
|
||||
],
|
||||
});
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async () => ({ ok: true }));
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey: agent.sessionKey,
|
||||
message: "new message",
|
||||
now: () => 1000,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
const optimisticUserAppend = dispatch.mock.calls
|
||||
.map((entry) => entry[0])
|
||||
.find(
|
||||
(action) =>
|
||||
action &&
|
||||
typeof action === "object" &&
|
||||
"type" in action &&
|
||||
action.type === "appendOutput" &&
|
||||
"line" in action &&
|
||||
action.line === "> new message"
|
||||
);
|
||||
expect(optimisticUserAppend).toBeTruthy();
|
||||
expect((optimisticUserAppend as { transcript?: { timestampMs?: number } }).transcript?.timestampMs).toBe(5001);
|
||||
});
|
||||
|
||||
it("uses_output_meta_timestamps_when_transcript_entries_are_missing", async () => {
|
||||
const sessionKey = "agent:agent-1:studio:test-session";
|
||||
const agent = createAgent({
|
||||
sessionSettingsSynced: true,
|
||||
transcriptEntries: undefined,
|
||||
outputLines: [
|
||||
formatMetaMarkdown({ role: "assistant", timestamp: 12_000 }),
|
||||
"previous assistant",
|
||||
],
|
||||
});
|
||||
const dispatch = vi.fn();
|
||||
const call = vi.fn(async () => ({ ok: true }));
|
||||
|
||||
await sendChatMessageViaStudio({
|
||||
client: { call },
|
||||
dispatch,
|
||||
getAgent: () => agent,
|
||||
agentId: agent.agentId,
|
||||
sessionKey,
|
||||
message: "new message",
|
||||
now: () => 1000,
|
||||
generateRunId: () => "run-1",
|
||||
});
|
||||
|
||||
const optimisticUserAppend = dispatch.mock.calls
|
||||
.map((entry) => entry[0])
|
||||
.find(
|
||||
(action) =>
|
||||
action &&
|
||||
typeof action === "object" &&
|
||||
"type" in action &&
|
||||
action.type === "appendOutput" &&
|
||||
"line" in action &&
|
||||
action.line === "> new message"
|
||||
);
|
||||
expect(optimisticUserAppend).toBeTruthy();
|
||||
expect((optimisticUserAppend as { transcript?: { timestampMs?: number } }).transcript?.timestampMs).toBe(12_001);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user