Files
kandev/apps/web/lib/task-agent-error.test.ts
Carlos Florêncio 1abb1c914e fix: keep acknowledged agent errors hidden (#1558)
* fix: keep acknowledged agent errors hidden

* fix: address agent error acknowledgement review

* fix: include fallback sessions in error acknowledgements

* test: cover agent error acknowledgement helpers

---------

Co-authored-by: root <root@kandev.ts.net>
2026-06-30 16:29:46 +01:00

306 lines
9.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { Message, TaskSession } from "@/lib/types/http";
import { sessionId, taskId } from "@/lib/types/http";
import { lastAgentErrorStamp } from "@/lib/session-last-agent-error";
import {
agentErrorMessageForTask,
resolvedAgentErrorAcknowledgementStamp,
} from "./task-agent-error";
const ERROR_OCCURRED_AT = "2026-06-14T10:00:00Z";
const AGENT_MESSAGE_AFTER_ERROR_AT = "2026-06-14T10:00:01Z";
const PRIMARY_ERROR = "primary error";
const PRIMARY_TASK = { id: "task-1", primarySessionId: "primary" };
function session(overrides: Partial<TaskSession>): TaskSession {
return {
id: sessionId("session-1"),
task_id: taskId("task-1"),
state: "WAITING_FOR_INPUT",
started_at: "2026-06-14T10:00:00Z",
updated_at: "2026-06-14T10:00:00Z",
...overrides,
} as TaskSession;
}
function errorMetadata(message: string) {
return {
last_agent_error: {
message,
occurred_at: ERROR_OCCURRED_AT,
},
};
}
function primarySession(overrides: Partial<TaskSession> = {}): TaskSession {
return session({
id: sessionId("primary"),
metadata: errorMetadata(PRIMARY_ERROR),
...overrides,
});
}
function agentMessage(overrides: Partial<Message>): Message {
return {
id: "msg-1",
session_id: sessionId("primary"),
task_id: taskId("task-1"),
author_type: "agent",
content: "",
type: "agent_message",
created_at: "2026-06-14T11:00:00Z",
...overrides,
} as Message;
}
describe("agentErrorMessageForTask", () => {
it("uses the explicit primary session even when it is terminal", () => {
const primary = primarySession({ state: "COMPLETED" });
expect(agentErrorMessageForTask(PRIMARY_TASK, { primary }, { "task-1": [] })).toBe(
PRIMARY_ERROR,
);
});
it("ignores stale terminal sessions in the fallback path", () => {
const oldFailed = session({
id: sessionId("old-failed"),
state: "FAILED",
updated_at: "2026-06-14T12:00:00Z",
metadata: errorMetadata("old failure"),
});
const current = session({
id: sessionId("current"),
state: "WAITING_FOR_INPUT",
updated_at: "2026-06-14T11:00:00Z",
metadata: errorMetadata("current failure"),
});
expect(
agentErrorMessageForTask(
{ id: "task-1" },
{ "old-failed": oldFailed, current },
{ "task-1": [oldFailed, current] },
),
).toBe("current failure");
});
it("hides the error when the matching stamp is in the dismissed map", () => {
const primary = primarySession();
const stamp = lastAgentErrorStamp({
message: PRIMARY_ERROR,
occurredAt: ERROR_OCCURRED_AT,
});
expect(
agentErrorMessageForTask(
PRIMARY_TASK,
{ primary },
{ "task-1": [primary] },
{ dismissedAgentErrors: { primary: stamp } },
),
).toBeNull();
});
it("hides the error when the matching stamp is sidebar-acknowledged", () => {
const primary = primarySession();
const stamp = lastAgentErrorStamp({
message: PRIMARY_ERROR,
occurredAt: ERROR_OCCURRED_AT,
});
expect(
agentErrorMessageForTask(
PRIMARY_TASK,
{ primary },
{ "task-1": [primary] },
{ acknowledgedAgentErrors: { primary: stamp } },
),
).toBeNull();
});
});
describe("agentErrorMessageForTask (hide conditions)", () => {
it("hides the error when the session metadata is server-dismissed", () => {
const primary = primarySession({
metadata: {
last_agent_error: {
message: PRIMARY_ERROR,
occurred_at: ERROR_OCCURRED_AT,
dismissed_at: "2026-06-14T10:05:00Z",
},
},
});
expect(agentErrorMessageForTask(PRIMARY_TASK, { primary }, { "task-1": [primary] })).toBeNull();
});
it("keeps the error when only an older stamp is dismissed", () => {
const primary = primarySession();
expect(
agentErrorMessageForTask(
PRIMARY_TASK,
{ primary },
{ "task-1": [primary] },
{ dismissedAgentErrors: { primary: "stale-stamp" } },
),
).toBe(PRIMARY_ERROR);
});
it("hides the error once an agent message arrives after the error timestamp", () => {
const primary = primarySession();
expect(
agentErrorMessageForTask(
PRIMARY_TASK,
{ primary },
{ "task-1": [primary] },
{
messagesBySession: {
primary: [agentMessage({ created_at: AGENT_MESSAGE_AFTER_ERROR_AT })],
},
},
),
).toBeNull();
});
it("keeps the error when newer messages are from the user, not the agent", () => {
const primary = primarySession();
expect(
agentErrorMessageForTask(
PRIMARY_TASK,
{ primary },
{ "task-1": [primary] },
{
messagesBySession: {
primary: [
agentMessage({ author_type: "user", created_at: AGENT_MESSAGE_AFTER_ERROR_AT }),
],
},
},
),
).toBe(PRIMARY_ERROR);
});
});
describe("resolvedAgentErrorAcknowledgementStamp", () => {
it("returns a stamp when a later agent message makes the sidebar error stale", () => {
const primary = primarySession();
const stamp = lastAgentErrorStamp({
message: PRIMARY_ERROR,
occurredAt: ERROR_OCCURRED_AT,
});
expect(
resolvedAgentErrorAcknowledgementStamp("primary", primary, {
messagesBySession: {
primary: [agentMessage({ created_at: AGENT_MESSAGE_AFTER_ERROR_AT })],
},
}),
).toBe(stamp);
});
it("does not return a stamp when the sidebar acknowledgement is already stored", () => {
const primary = primarySession();
const stamp = lastAgentErrorStamp({
message: PRIMARY_ERROR,
occurredAt: ERROR_OCCURRED_AT,
});
expect(
resolvedAgentErrorAcknowledgementStamp("primary", primary, {
acknowledgedAgentErrors: { primary: stamp },
messagesBySession: {
primary: [agentMessage({ created_at: AGENT_MESSAGE_AFTER_ERROR_AT })],
},
}),
).toBeNull();
});
});
// Regression: string-based comparison of RFC3339 timestamps with mixed
// fractional-second precision (e.g. ".5" vs ".50") sorts incorrectly, which
// could leave a stale error icon visible or clear it too early. Numeric
// Date.parse() comparison treats them as equal moments in time.
describe("agentErrorMessageForTask (timestamp precision)", () => {
it("does not clear the error when an agent message shares the same instant", () => {
const errorAt = "2026-06-14T10:00:00.5Z";
const messageAt = "2026-06-14T10:00:00.50Z"; // same instant, longer fractional
const primary = primarySession({
metadata: { last_agent_error: { message: PRIMARY_ERROR, occurred_at: errorAt } },
});
expect(
agentErrorMessageForTask(
PRIMARY_TASK,
{ primary },
{ "task-1": [primary] },
{ messagesBySession: { primary: [agentMessage({ created_at: messageAt })] } },
),
).toBe(PRIMARY_ERROR);
});
it("clears the error when an agent message is strictly later despite shorter fractional precision", () => {
const errorAt = "2026-06-14T10:00:00.999Z";
const messageAt = "2026-06-14T10:00:01.0Z";
const primary = primarySession({
metadata: { last_agent_error: { message: PRIMARY_ERROR, occurred_at: errorAt } },
});
expect(
agentErrorMessageForTask(
PRIMARY_TASK,
{ primary },
{ "task-1": [primary] },
{ messagesBySession: { primary: [agentMessage({ created_at: messageAt })] } },
),
).toBeNull();
});
});
// Coverage for the fallback `sessionsByTaskId` path (no primarySessionId).
// shouldHideError is shared with the primary path, but exercising it through
// the fallback loop guards against any regression in the per-branch wiring.
describe("agentErrorMessageForTask (fallback session path)", () => {
const FALLBACK_TASK = { id: "task-1" };
const FALLBACK_ERROR = "fallback failure";
function fallbackSession() {
return session({
id: sessionId("fallback"),
metadata: errorMetadata(FALLBACK_ERROR),
});
}
it("hides the error when the matching stamp is dismissed", () => {
const fallback = fallbackSession();
const stamp = lastAgentErrorStamp({
message: FALLBACK_ERROR,
occurredAt: ERROR_OCCURRED_AT,
});
expect(
agentErrorMessageForTask(
FALLBACK_TASK,
{ fallback },
{ "task-1": [fallback] },
{ dismissedAgentErrors: { fallback: stamp } },
),
).toBeNull();
});
it("hides the error once a later agent message arrives", () => {
const fallback = fallbackSession();
expect(
agentErrorMessageForTask(
FALLBACK_TASK,
{ fallback },
{ "task-1": [fallback] },
{
messagesBySession: {
fallback: [
agentMessage({
session_id: sessionId("fallback"),
created_at: "2026-06-14T11:00:00Z",
}),
],
},
},
),
).toBeNull();
});
});