Files
multica/packages/core/api/schemas.test.ts
Multica Eve c25a82eee0 perf(agents): fast model discovery on runtime switch (MUL-5444) (#6098)
* perf(agents): make runtime model discovery fast on runtime switch (MUL-5444)

Switching runtime in the agent creation form left the model picker
spinning for ~8-20s. Two costs stacked up:

- the list-models request sat in the store until the daemon's next
  scheduled heartbeat (0-15s, avg 7.5s of pure dead wait), and
- the daemon then enumerated the catalog locally (static for claude,
  but a CLI/ACP round trip up to ~15s for everyone else).

Both are addressed with the two standard techniques for a slow,
low-frequency, read-only operation: push instead of poll, and
stale-while-revalidate.

Push (removes the heartbeat wait):
- new additive `daemon:pending_work` hint, runtime-scoped, delivered
  through the existing daemon WS hub and the Redis relay so the API node
  holding the socket does the delivery.
- the daemon answers a hint with ONE immediate heartbeat and dispatches
  what it claimed. The hint deliberately carries no work, so nothing has
  to be un-claimed when delivery fails and a duplicate hint cannot
  duplicate work - PopPending stays the atomic claim.
- per-runtime coalescing plus a 1s floor keeps a caller-triggered hint
  from becoming a heartbeat amplifier.

Cache (removes the discovery wait on repeat opens):
- server-side per-runtime catalog cache (in-memory single-node, Redis
  multi-node) written on every successful report.
- a snapshot younger than 15min answers the POST immediately as an
  already-completed request; older than 60s it also enqueues a
  background refresh that only warms the cache.
- only supported, non-empty catalogs are cached; a completed-but-empty
  report invalidates instead, while a failed report keeps serving the
  last known good list.

Frontend: staleTime 60s -> 5min and gcTime 30min, so a runtime revisited
in the same session renders from cache and revalidates in the background
instead of showing the spinner again.

Compatibility: every wire change is additive. Old daemons ignore the
unknown hint type and keep using the scheduled heartbeat; new daemons
against an old server simply never receive one. The cached response is
shaped exactly like a completed live discovery apart from the optional
`cached` / `cached_at` markers.

Co-authored-by: multica-agent <github@multica.ai>

* fix(agents): address review on model discovery SWR (MUL-5444)

Sol-Boy's review on #6098 found the client cache could outlive the
server's own staleness promise, and that the two changed endpoints were
still cast rather than validated.

Must-fix 1 — client freshness now derives from the served answer.
`staleTime` was a flat 5min, so a 14-minute-old snapshot (which the
server returns while queueing its own refresh) was held as fresh for
another 5min: observable staleness became server window + client window,
and the refreshed catalog never reached the tab that triggered the
refresh. `staleTime` is now a function of the query data: a `cached`
answer is stale on arrival (bound stays the server's window alone, and
the next mount/focus picks up the refreshed snapshot), while a live
discovery — which just measured the truth — is trusted for the full 5min
so a cold runtime is never re-enumerated inside one form session.
`gcTime` stays 30min, so a revisited runtime still renders from cache and
revalidates in the background; the pickers gate their spinner on
`isLoading`, which stays false throughout.

Must-fix 2 — both model-discovery responses go through a zod schema.
`POST /api/runtimes/{id}/models` and its poll companion were casting
network JSON to `RuntimeModelListRequest`, which the root CLAUDE.md
API-compatibility rules forbid. Added a lenient schema (`status` stays
`z.string()`, `supported` defaults to true, `.loose()` keeps unknown
fields) plus a fallback record whose `status` is `failed`: a malformed
body now surfaces "discovery failed" with manual entry still usable
instead of a fabricated empty catalog or an endless spinner.
`resolveRuntimeModels` was tightened to match — only an explicit
`completed` is a catalog, so an unrecognised status is an error rather
than a silent empty list, and `supported` can no longer be `undefined`.

Nit — the in-memory catalog cache now deep-copies each entry's
`Thinking` (and its level slice) and `ServiceTiers`, so it delivers the
independent value its comment promises and matches the Redis backend's
JSON round-trip semantics.

Tests: staleTime policy for cached/live/no-data; a QueryObserver test
proving the refreshed catalog reaches the same client with no blank
loading state; unknown-status and omitted-`supported` handling; schema
tests for live, cached, old-backend and nine malformed shapes; client
tests that both endpoints degrade to an explicit failure; nested-field
mutation isolation for the cache.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-29 16:03:26 +08:00

1110 lines
38 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import {
AppConfigSchema,
AgentTaskListSchema,
AutopilotRunSchema,
FALLBACK_AUTOPILOT_RUN,
CommentTriggerPreviewSchema,
DashboardAgentRunTimeListSchema,
DashboardFailureByAgentListSchema,
DashboardFailureDailyListSchema,
DashboardUsageByAgentListSchema,
DashboardUsageDailyListSchema,
ChatDraftRestoresResponseSchema,
CreateFeedbackResponseSchema,
DuplicateIssueErrorBodySchema,
EMPTY_CHAT_DRAFT_RESTORES,
EMPTY_CREATE_FEEDBACK_RESPONSE,
EMPTY_INBOX_ITEMS,
EMPTY_INBOX_UNREAD_SUMMARY,
EMPTY_SEARCH_PROJECTS_RESPONSE,
EMPTY_USER,
InboxItemListSchema,
InboxUnreadSummarySchema,
IssueTriggerPreviewSchema,
ListIssuesResponseSchema,
ListPropertiesResponseSchema,
MALFORMED_RUNTIME_MODEL_LIST_REQUEST,
RuntimeModelListRequestSchema,
SearchProjectsResponseSchema,
RuntimeHourlyActivityListSchema,
RuntimeUsageByAgentListSchema,
RuntimeUsageByHourListSchema,
RuntimeUsageListSchema,
SquadListSchema,
SquadSchema,
TimelineEntriesSchema,
UserSchema,
} from "./schemas";
import { parseWithFallback } from "./schema";
const baseIssue = {
id: "11111111-1111-1111-1111-111111111111",
workspace_id: "ws-1",
number: 1,
identifier: "MUL-1",
title: "Test",
description: null,
status: "todo",
priority: "medium",
assignee_type: null,
assignee_id: null,
creator_type: "member",
creator_id: "user-1",
parent_issue_id: null,
project_id: null,
position: 0,
stage: null,
start_date: null,
due_date: null,
metadata: {},
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
};
describe("IssueSchema (via ListIssuesResponseSchema)", () => {
it("accepts a primitive metadata KV map", () => {
const payload = {
issues: [
{
...baseIssue,
metadata: { pipeline_status: "waiting", pr_number: 3, is_blocked: true },
},
],
total: 1,
};
const parsed = ListIssuesResponseSchema.parse(payload);
expect(parsed.issues[0]?.metadata).toEqual({
pipeline_status: "waiting",
pr_number: 3,
is_blocked: true,
});
});
it("defaults metadata to {} when the server omits it (older backend)", () => {
const { metadata: _omit, ...issueWithoutMetadata } = baseIssue;
const payload = { issues: [issueWithoutMetadata], total: 1 };
const parsed = ListIssuesResponseSchema.parse(payload);
expect(parsed.issues[0]?.metadata).toEqual({});
});
it("rejects metadata with non-primitive values (nested object)", () => {
const payload = {
issues: [{ ...baseIssue, metadata: { nested: { x: 1 } } }],
total: 1,
};
expect(ListIssuesResponseSchema.safeParse(payload).success).toBe(false);
});
it("accepts a numeric stage", () => {
const payload = { issues: [{ ...baseIssue, stage: 2 }], total: 1 };
const parsed = ListIssuesResponseSchema.parse(payload);
expect(parsed.issues[0]?.stage).toBe(2);
});
it("defaults stage to null when the server omits it (older backend)", () => {
const { stage: _omit, ...issueWithoutStage } = baseIssue;
const payload = { issues: [issueWithoutStage], total: 1 };
const parsed = ListIssuesResponseSchema.parse(payload);
expect(parsed.issues[0]?.stage).toBeNull();
});
it("accepts custom property values including multi_select arrays", () => {
const payload = {
issues: [
{
...baseIssue,
properties: { "def-1": "opt-a", "def-2": ["opt-x", "opt-y"], "def-3": 3.5, "def-4": true },
},
],
total: 1,
};
const parsed = ListIssuesResponseSchema.parse(payload);
expect(parsed.issues[0]?.properties).toEqual({
"def-1": "opt-a",
"def-2": ["opt-x", "opt-y"],
"def-3": 3.5,
"def-4": true,
});
});
it("defaults properties to {} when the server omits it (older backend)", () => {
const parsed = ListIssuesResponseSchema.parse({ issues: [baseIssue], total: 1 });
expect(parsed.issues[0]?.properties).toEqual({});
});
it("drops unknown-shaped property values instead of failing the issue parse", () => {
// Forward compat: a future server type (actor/relation) may ship object
// values. That one entry must disappear; the issue and its other
// properties must survive — a full parse failure would blank the whole
// list through parseWithFallback on installed desktop builds.
const payload = {
issues: [
{
...baseIssue,
properties: { "def-1": { nested: 1 }, "def-2": "opt-a" },
},
],
total: 1,
};
const parsed = ListIssuesResponseSchema.parse(payload);
expect(parsed.issues[0]?.properties).toEqual({ "def-2": "opt-a" });
});
});
describe("IssuePropertySchema (via ListPropertiesResponseSchema)", () => {
const baseProperty = {
id: "22222222-2222-2222-2222-222222222222",
workspace_id: "ws-1",
name: "Severity",
type: "select",
description: "",
icon: "flag",
config: { options: [{ id: "opt-1", name: "Critical", color: "#ef4444" }] },
position: 1,
archived: false,
archived_at: null,
usage_count: 2,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
};
it("parses a full definition", () => {
const parsed = ListPropertiesResponseSchema.parse({ properties: [baseProperty], total: 1 });
expect(parsed.properties[0]?.config.options?.[0]?.name).toBe("Critical");
expect(parsed.properties[0]?.icon).toBe("flag");
});
it("survives a malformed response by defaulting the list", () => {
const parsed = ListPropertiesResponseSchema.parse({});
expect(parsed.properties).toEqual([]);
expect(parsed.total).toBe(0);
});
it("keeps unknown property types as strings (forward compat)", () => {
const parsed = ListPropertiesResponseSchema.parse({
properties: [{ ...baseProperty, type: "relation", config: {} }],
total: 1,
});
expect(parsed.properties[0]?.type).toBe("relation");
});
it("defaults config when the server sends none", () => {
const { config: _omit, ...withoutConfig } = baseProperty;
const parsed = ListPropertiesResponseSchema.parse({ properties: [withoutConfig], total: 1 });
expect(parsed.properties[0]?.config).toEqual({});
});
it("defaults icon for an older server response", () => {
const { icon: _omit, ...withoutIcon } = baseProperty;
const parsed = ListPropertiesResponseSchema.parse({ properties: [withoutIcon], total: 1 });
expect(parsed.properties[0]?.icon).toBe("");
});
});
// POST /api/issues/preview-trigger feeds this schema through parseWithFallback
// in client.previewIssueTrigger with fallback { triggers: [], total_count: 0 }
// (MUL-3375). The four entry points read it to decide "will this start a run",
// so malformed / missing / null drift must degrade to "nothing will start"
// rather than throw into the picker/modal.
const PREVIEW_FALLBACK = { triggers: [], total_count: 0 };
const PREVIEW_ENDPOINT = { endpoint: "POST /api/issues/preview-trigger" };
describe("IssueTriggerPreviewSchema", () => {
it("parses a well-formed response", () => {
const parsed = IssueTriggerPreviewSchema.parse({
triggers: [
{ issue_id: "i1", agent_id: "a1", source: "assign", handoff_supported: true },
{ issue_id: "i2", agent_id: "a2", source: "status", handoff_supported: false },
],
total_count: 2,
});
expect(parsed.total_count).toBe(2);
expect(parsed.triggers).toHaveLength(2);
expect(parsed.triggers[0]).toMatchObject({ issue_id: "i1", agent_id: "a1", source: "assign", handoff_supported: true });
});
it("defaults missing top-level fields (empty / older backend)", () => {
const parsed = IssueTriggerPreviewSchema.parse({});
expect(parsed.triggers).toEqual([]);
expect(parsed.total_count).toBe(0);
});
it("defaults missing optional item fields, keeping required issue_id", () => {
const parsed = IssueTriggerPreviewSchema.parse({ triggers: [{ issue_id: "i1" }], total_count: 1 });
expect(parsed.triggers[0]).toEqual({
issue_id: "i1",
agent_id: "",
source: "",
handoff_supported: false,
});
});
it("parseWithFallback returns the fallback for a malformed shape (triggers not an array)", () => {
const parsed = parseWithFallback(
{ triggers: "nope", total_count: 1 },
IssueTriggerPreviewSchema,
PREVIEW_FALLBACK,
PREVIEW_ENDPOINT,
);
expect(parsed).toEqual(PREVIEW_FALLBACK);
});
it("parseWithFallback returns the fallback when an item drops the required issue_id", () => {
const parsed = parseWithFallback(
{ triggers: [{ agent_id: "a1", source: "assign" }], total_count: 1 },
IssueTriggerPreviewSchema,
PREVIEW_FALLBACK,
PREVIEW_ENDPOINT,
);
expect(parsed).toEqual(PREVIEW_FALLBACK);
});
it("parseWithFallback returns the fallback for a wrong-typed total_count", () => {
const parsed = parseWithFallback(
{ triggers: [], total_count: "5" },
IssueTriggerPreviewSchema,
PREVIEW_FALLBACK,
PREVIEW_ENDPOINT,
);
expect(parsed).toEqual(PREVIEW_FALLBACK);
});
it("parseWithFallback returns the fallback for null / non-object bodies", () => {
expect(parseWithFallback(null, IssueTriggerPreviewSchema, PREVIEW_FALLBACK, PREVIEW_ENDPOINT)).toEqual(PREVIEW_FALLBACK);
expect(parseWithFallback("oops", IssueTriggerPreviewSchema, PREVIEW_FALLBACK, PREVIEW_ENDPOINT)).toEqual(PREVIEW_FALLBACK);
});
});
describe("TimelineEntriesSchema", () => {
it("preserves source_task_id for agent failure comments", () => {
const parsed = TimelineEntriesSchema.parse([
{
type: "comment",
id: "comment-1",
actor_type: "agent",
actor_id: "agent-1",
created_at: "2026-01-01T00:00:00Z",
content: "API Error: 500 Internal server error",
comment_type: "system",
source_task_id: "task-1",
},
]);
expect(parsed[0]?.source_task_id).toBe("task-1");
});
});
describe("AgentTaskListSchema", () => {
const task = {
id: "task-1",
agent_id: "agent-1",
runtime_id: "runtime-1",
issue_id: "issue-1",
status: "queued",
priority: 0,
dispatched_at: null,
started_at: null,
completed_at: null,
result: null,
error: null,
created_at: "2026-07-10T00:00:00Z",
trigger_comment_id: "comment-3",
};
it("preserves planned and delivered comment IDs for a task run", () => {
const parsed = AgentTaskListSchema.parse([
{
...task,
coalesced_comment_ids: ["comment-1", "comment-2"],
delivered_comment_ids: ["comment-1", "comment-2", "comment-3"],
},
]);
expect(parsed[0]?.trigger_comment_id).toBe("comment-3");
expect(parsed[0]?.coalesced_comment_ids).toEqual([
"comment-1",
"comment-2",
]);
expect(parsed[0]?.delivered_comment_ids).toEqual([
"comment-1",
"comment-2",
"comment-3",
]);
});
it("accepts task payloads from older backends without comment coverage", () => {
const parsed = AgentTaskListSchema.parse([task]);
expect(parsed[0]?.coalesced_comment_ids).toBeUndefined();
expect(parsed[0]?.delivered_comment_ids).toBeUndefined();
});
it("degrades malformed optional coverage without dropping task rows", () => {
const parsed = AgentTaskListSchema.parse([
{
...task,
coalesced_comment_ids: ["comment-1", 2],
delivered_comment_ids: "not-an-array",
},
{
...task,
id: "task-2",
delivered_comment_ids: ["comment-2", "comment-3"],
},
]);
expect(parsed).toHaveLength(2);
expect(parsed[0]?.coalesced_comment_ids).toBeUndefined();
expect(parsed[0]?.delivered_comment_ids).toBeUndefined();
expect(parsed[1]?.delivered_comment_ids).toEqual([
"comment-2",
"comment-3",
]);
});
});
describe("ChatDraftRestoresResponseSchema", () => {
it("parses a well-formed response with attachments", () => {
const parsed = parseWithFallback(
{
restores: [
{
id: "msg-1",
chat_session_id: "s-1",
task_id: "t-1",
content: "run the thing",
attachments: [{ id: "att-1", filename: "notes.txt" }],
created_at: "2026-07-01T00:00:00Z",
},
],
},
ChatDraftRestoresResponseSchema,
EMPTY_CHAT_DRAFT_RESTORES,
{ endpoint: "test" },
);
expect(parsed.restores).toHaveLength(1);
expect(parsed.restores[0]?.content).toBe("run the thing");
expect(parsed.restores[0]?.attachments?.[0]?.id).toBe("att-1");
});
it("defaults a missing restores array instead of crashing the composer", () => {
const parsed = parseWithFallback(
{},
ChatDraftRestoresResponseSchema,
EMPTY_CHAT_DRAFT_RESTORES,
{ endpoint: "test" },
);
expect(parsed.restores).toEqual([]);
});
it("falls back to the empty response on a malformed row", () => {
// A row without the consume key (id) is unusable — the whole response
// falls back and the durable rows simply stay pending server-side.
const parsed = parseWithFallback(
{ restores: [{ chat_session_id: "s-1", content: 42 }] },
ChatDraftRestoresResponseSchema,
EMPTY_CHAT_DRAFT_RESTORES,
{ endpoint: "test" },
);
expect(parsed).toEqual(EMPTY_CHAT_DRAFT_RESTORES);
});
});
describe("CreateFeedbackResponseSchema", () => {
const ENDPOINT = { endpoint: "POST /api/feedback" };
it("parses a well-formed response and preserves extra fields", () => {
const parsed = parseWithFallback(
{ id: "feedback-1", created_at: "2026-06-26T00:00:00Z", future_field: true },
CreateFeedbackResponseSchema,
EMPTY_CREATE_FEEDBACK_RESPONSE,
ENDPOINT,
);
expect(parsed).toMatchObject({
id: "feedback-1",
created_at: "2026-06-26T00:00:00Z",
future_field: true,
});
});
it("returns the empty fallback for malformed feedback responses", () => {
expect(
parseWithFallback(
{ id: 123, created_at: "2026-06-26T00:00:00Z" },
CreateFeedbackResponseSchema,
EMPTY_CREATE_FEEDBACK_RESPONSE,
ENDPOINT,
),
).toBe(EMPTY_CREATE_FEEDBACK_RESPONSE);
expect(
parseWithFallback(null, CreateFeedbackResponseSchema, EMPTY_CREATE_FEEDBACK_RESPONSE, ENDPOINT),
).toBe(EMPTY_CREATE_FEEDBACK_RESPONSE);
});
});
// The duplicate-issue branch in create-issue.tsx feeds ApiError.body
// (typed as `unknown`) through this schema. Any future server drift that
// loses the contract MUST fail the parse so the UI falls back to a normal
// error toast instead of rendering an empty / partial duplicate card.
describe("DuplicateIssueErrorBodySchema", () => {
const valid = {
code: "active_duplicate_issue",
error: "An active issue with this title already exists: MUL-12 Login bug",
issue: {
id: "11111111-1111-1111-1111-111111111111",
identifier: "MUL-12",
title: "Login bug",
},
};
it("accepts a well-formed body", () => {
expect(DuplicateIssueErrorBodySchema.safeParse(valid).success).toBe(true);
});
it("accepts unknown extra fields via .loose()", () => {
const forwardCompat = {
...valid,
hint: "Try a different title",
issue: { ...valid.issue, workspace_id: "ws-1", status: "todo" },
};
expect(DuplicateIssueErrorBodySchema.safeParse(forwardCompat).success).toBe(true);
});
it("rejects a renamed code (so renames degrade to the generic toast)", () => {
const renamed = { ...valid, code: "duplicate_issue" };
expect(DuplicateIssueErrorBodySchema.safeParse(renamed).success).toBe(false);
});
it("rejects a missing issue object", () => {
const { issue: _omit, ...without } = valid;
expect(DuplicateIssueErrorBodySchema.safeParse(without).success).toBe(false);
});
it("rejects a non-string issue.id", () => {
const broken = { ...valid, issue: { ...valid.issue, id: 42 } };
expect(DuplicateIssueErrorBodySchema.safeParse(broken).success).toBe(false);
});
it("accepts a missing error field (it is optional)", () => {
const { error: _omit, ...without } = valid;
expect(DuplicateIssueErrorBodySchema.safeParse(without).success).toBe(true);
});
});
// `user.timezone` (Viewing tz) was added in the timezone-architecture RFC.
// A desktop build older than the server — or a server predating the
// `user.timezone` migration — will return a `/api/me` body with no
// `timezone` key. The schema must not fail closed on that: the field
// defaults to `null`, which the frontend resolves to the browser-detected
// tz at render time.
describe("UserSchema timezone drift", () => {
const base = {
id: "11111111-1111-1111-1111-111111111111",
name: "Ada",
email: "ada@example.com",
};
it("defaults timezone to null when the field is absent", () => {
const parsed = UserSchema.parse(base);
expect(parsed.timezone).toBe(null);
});
it("preserves an explicit IANA timezone", () => {
const parsed = UserSchema.parse({ ...base, timezone: "Asia/Tokyo" });
expect(parsed.timezone).toBe("Asia/Tokyo");
});
it("accepts an explicit null timezone", () => {
const parsed = UserSchema.parse({ ...base, timezone: null });
expect(parsed.timezone).toBe(null);
});
// Wrong-type drift: a future server bug sending `timezone` as a number
// must not throw into the UI. parseWithFallback degrades the whole user
// object to the explicit fallback (EMPTY_USER) so /api/me callers keep a
// valid shape instead of white-screening.
it("falls back to EMPTY_USER when timezone is the wrong type", () => {
const parsed = parseWithFallback(
{ ...base, timezone: 42 },
UserSchema,
EMPTY_USER,
{ endpoint: "GET /api/me" },
);
expect(parsed).toBe(EMPTY_USER);
});
});
describe("SquadListSchema member preview drift", () => {
const baseSquad = {
id: "squad-1",
workspace_id: "ws-1",
name: "Frontend Squad",
description: "",
instructions: "",
avatar_url: null,
leader_id: "agent-1",
creator_id: "user-1",
created_at: "2026-05-01T00:00:00Z",
updated_at: "2026-05-01T00:00:00Z",
archived_at: null,
archived_by: null,
};
it("defaults preview fields when an older backend omits them", () => {
const parsed = SquadListSchema.parse([baseSquad]);
expect(parsed[0]?.member_count).toBe(0);
expect(parsed[0]?.member_preview).toEqual([]);
});
it("defaults preview fields on a single squad response", () => {
const parsed = SquadSchema.parse(baseSquad);
expect(parsed.member_count).toBe(0);
expect(parsed.member_preview).toEqual([]);
});
it("preserves lightweight member preview rows", () => {
const parsed = SquadListSchema.parse([
{
...baseSquad,
member_count: 2,
member_preview: [
{ member_type: "agent", member_id: "agent-1", role: "leader" },
{ member_type: "member", member_id: "user-2", role: "member" },
],
},
]);
expect(parsed[0]?.member_count).toBe(2);
expect(parsed[0]?.member_preview).toHaveLength(2);
expect(parsed[0]?.member_preview?.[0]?.role).toBe("leader");
});
});
// The workspace dashboard and runtime-detail pages were re-pointed at the
// unified `task_usage_hourly` rollup. Every numeric field drives chart /
// KPI math, and string keys (date / agent_id / model) bucket the series.
// The contract these schemas must hold: a row missing a field degrades
// that field to a sane default rather than dropping the WHOLE array to
// the `[]` fallback — one drifted row must not blank the entire chart.
describe("dashboard + runtime usage schema drift", () => {
it("coerces a missing numeric field to 0 instead of dropping the array", () => {
const parsed = DashboardUsageDailyListSchema.parse([
{ date: "2026-05-19", model: "claude-opus-4-7", input_tokens: 100 },
]);
expect(parsed).toHaveLength(1);
expect(parsed[0]?.output_tokens).toBe(0);
expect(parsed[0]?.cache_read_tokens).toBe(0);
expect(parsed[0]?.cache_write_tokens).toBe(0);
});
it("coerces a missing date key to \"\" so the rest of the series survives", () => {
const parsed = DashboardUsageDailyListSchema.parse([
{ model: "claude-opus-4-7", input_tokens: 5 },
]);
expect(parsed).toHaveLength(1);
expect(parsed[0]?.date).toBe("");
});
it("coerces a missing agent_id key to \"\" for the agent-runtime panel", () => {
const parsed = DashboardAgentRunTimeListSchema.parse([
{ total_seconds: 42, task_count: 3, failed_count: 0 },
]);
expect(parsed).toHaveLength(1);
expect(parsed[0]?.agent_id).toBe("");
});
it("coerces a missing agent_id key to \"\" for the usage-by-agent panel", () => {
const parsed = DashboardUsageByAgentListSchema.parse([
{ model: "claude-opus-4-7", input_tokens: 7 },
]);
expect(parsed[0]?.agent_id).toBe("");
});
it("coerces missing fields on every runtime usage schema", () => {
expect(RuntimeUsageListSchema.parse([{ date: "2026-05-19" }])[0]?.input_tokens).toBe(0);
expect(RuntimeHourlyActivityListSchema.parse([{ hour: 9 }])[0]?.count).toBe(0);
expect(RuntimeUsageByAgentListSchema.parse([{ model: "x" }])[0]?.agent_id).toBe("");
expect(RuntimeUsageByHourListSchema.parse([{ hour: 9 }])[0]?.model).toBe("");
});
it("defaults a missing provider to \"\" so an older server's rows still price by bare model", () => {
// provider was added for cross-provider model disambiguation; a server
// predating it omits the field. The schema must fill "" (→ bare-model
// pricing lookup) rather than drop the row.
expect(
DashboardUsageDailyListSchema.parse([{ date: "2026-05-19", model: "claude-opus-4-7" }])[0]
?.provider,
).toBe("");
expect(
DashboardUsageByAgentListSchema.parse([{ model: "claude-opus-4-7" }])[0]?.provider,
).toBe("");
expect(RuntimeUsageByAgentListSchema.parse([{ model: "x" }])[0]?.provider).toBe("");
});
it("rejects a non-array body so parseWithFallback can return its fallback", () => {
expect(DashboardUsageDailyListSchema.safeParse(null).success).toBe(false);
expect(DashboardFailureDailyListSchema.safeParse(null).success).toBe(false);
expect(DashboardFailureByAgentListSchema.safeParse({ rows: [] }).success).toBe(
false,
);
expect(RuntimeUsageListSchema.safeParse({ rows: [] }).success).toBe(false);
});
it("keeps a failure_reason the client build has never heard of", () => {
// failure_reason is an open string, not an enum: the backend taxonomy
// grows, and an installed desktop client must still count a reason its
// build predates rather than dropping the row (and with it the day's
// error total).
const parsed = DashboardFailureDailyListSchema.parse([
{ date: "2026-05-19", failure_reason: "agent_error.brand_new", task_count: 3 },
]);
expect(parsed).toHaveLength(1);
expect(parsed[0]?.failure_reason).toBe("agent_error.brand_new");
expect(parsed[0]?.task_count).toBe(3);
});
it("coerces a missing failure row field without dropping the array", () => {
const daily = DashboardFailureDailyListSchema.parse([{ date: "2026-05-19" }]);
expect(daily).toHaveLength(1);
// "" is the succeeded bucket, so a reason-less row lands in the
// denominator instead of inventing a failure that never happened.
//
// Defaulting to a failure bucket instead was considered and rejected: the
// realistic drift here is someone adding `omitempty` to the Go struct
// tag, which would strip the field from exactly the SUCCESS rows and turn
// every window into a 100% error rate. Deflating a rate under drift is
// the milder failure. TestDashboardFailureWireContractKeepsEmptyReason
// (server/internal/handler/dashboard_test.go) guards the other side by
// pinning that the server always emits the field.
expect(daily[0]?.failure_reason).toBe("");
expect(daily[0]?.task_count).toBe(0);
const byAgent = DashboardFailureByAgentListSchema.parse([
{ failure_reason: "timeout", task_count: 2 },
]);
expect(byAgent[0]?.agent_id).toBe("");
});
it("keeps unknown server-side fields via .loose()", () => {
const parsed = RuntimeUsageListSchema.parse([
{ date: "2026-05-19", region: "us-east" },
]);
expect((parsed[0] as Record<string, unknown>).region).toBe("us-east");
});
});
describe("AppConfigSchema cdn_signed drift", () => {
it("defaults cdn_signed to false when the server omits it (pre-MUL-3254 servers)", () => {
const parsed = AppConfigSchema.parse({ cdn_domain: "cdn.example.com" });
expect(parsed.cdn_signed).toBe(false);
});
it("coerces a malformed cdn_signed to false instead of failing the whole config", () => {
const parsed = AppConfigSchema.parse({
cdn_domain: "cdn.example.com",
cdn_signed: "yes",
});
expect(parsed.cdn_signed).toBe(false);
expect(parsed.cdn_domain).toBe("cdn.example.com");
});
it("keeps cdn_signed=true from a signing-enabled server", () => {
const parsed = AppConfigSchema.parse({ cdn_signed: true });
expect(parsed.cdn_signed).toBe(true);
});
it("parses frontend feature flag decisions", () => {
const parsed = AppConfigSchema.parse({
feature_flags: {
composio_mcp_apps: true,
malformed_future_flag: "yes",
},
});
expect(parsed.feature_flags).toEqual({
composio_mcp_apps: true,
malformed_future_flag: false,
});
});
it("defaults malformed feature_flags to an empty object", () => {
const parsed = AppConfigSchema.parse({ feature_flags: ["not", "an", "object"] });
expect(parsed.feature_flags).toEqual({});
});
it("parses server_version and leaves it undefined when the server omits it", () => {
expect(AppConfigSchema.parse({ server_version: "1.2.3" }).server_version).toBe("1.2.3");
expect(AppConfigSchema.parse({}).server_version).toBeUndefined();
});
});
describe("InboxUnreadSummarySchema", () => {
const ENDPOINT = { endpoint: "GET /api/inbox/unread-summary" };
it("parses a well-formed summary and tolerates extra fields", () => {
const parsed = parseWithFallback(
[
{ workspace_id: "ws-1", count: 2 },
{ workspace_id: "ws-2", count: 0, future_field: "ignored" },
],
InboxUnreadSummarySchema,
EMPTY_INBOX_UNREAD_SUMMARY,
ENDPOINT,
);
expect(parsed).toEqual([
{ workspace_id: "ws-1", count: 2 },
{ workspace_id: "ws-2", count: 0, future_field: "ignored" },
]);
});
it("returns the empty fallback (dot hidden) for a non-array body", () => {
expect(
parseWithFallback({ rows: [] }, InboxUnreadSummarySchema, EMPTY_INBOX_UNREAD_SUMMARY, ENDPOINT),
).toBe(EMPTY_INBOX_UNREAD_SUMMARY);
expect(
parseWithFallback(null, InboxUnreadSummarySchema, EMPTY_INBOX_UNREAD_SUMMARY, ENDPOINT),
).toBe(EMPTY_INBOX_UNREAD_SUMMARY);
});
it("returns the empty fallback when an entry has a wrong-typed count", () => {
expect(
parseWithFallback(
[{ workspace_id: "ws-1", count: "lots" }],
InboxUnreadSummarySchema,
EMPTY_INBOX_UNREAD_SUMMARY,
ENDPOINT,
),
).toBe(EMPTY_INBOX_UNREAD_SUMMARY);
});
});
describe("InboxItemListSchema", () => {
const ENDPOINT = { endpoint: "GET /api/inbox/archived" };
const row = (overrides: Record<string, unknown> = {}) => ({
id: "inbox-1",
workspace_id: "ws-1",
recipient_type: "member",
recipient_id: "member-1",
type: "new_comment",
severity: "info",
issue_id: "issue-1",
title: "Issue title",
body: null,
read: false,
archived: true,
created_at: "2026-06-15T08:00:00Z",
...overrides,
});
it("parses a well-formed archived list and tolerates extra fields", () => {
const parsed = parseWithFallback(
[row({ issue_status: "in_progress", details: { comment_id: "c-1" }, future_field: 1 })],
InboxItemListSchema,
EMPTY_INBOX_ITEMS,
ENDPOINT,
);
expect(parsed).toHaveLength(1);
expect(parsed[0]).toMatchObject({ id: "inbox-1", archived: true });
});
it("keeps a notification type this client doesn't know yet", () => {
// Enums stay lenient on purpose: a backend that ships a new inbox type
// must not blank the whole archived list on older clients.
const parsed = parseWithFallback(
[row({ type: "some_future_type", severity: "future_severity" })],
InboxItemListSchema,
EMPTY_INBOX_ITEMS,
ENDPOINT,
);
expect(parsed).toHaveLength(1);
});
it("accepts rows that omit the nullable optional fields", () => {
const { body, issue_id, ...withoutOptionals } = row();
void body;
void issue_id;
expect(
parseWithFallback([withoutOptionals], InboxItemListSchema, EMPTY_INBOX_ITEMS, ENDPOINT),
).toHaveLength(1);
});
it("returns the empty fallback for a non-array body", () => {
expect(
parseWithFallback({ items: [] }, InboxItemListSchema, EMPTY_INBOX_ITEMS, ENDPOINT),
).toBe(EMPTY_INBOX_ITEMS);
expect(
parseWithFallback(null, InboxItemListSchema, EMPTY_INBOX_ITEMS, ENDPOINT),
).toBe(EMPTY_INBOX_ITEMS);
});
it("returns the empty fallback when a row is missing a required field", () => {
const { id, ...withoutId } = row();
void id;
expect(
parseWithFallback([withoutId], InboxItemListSchema, EMPTY_INBOX_ITEMS, ENDPOINT),
).toBe(EMPTY_INBOX_ITEMS);
});
it("returns the empty fallback when `archived` is wrong-typed", () => {
expect(
parseWithFallback(
[row({ archived: "yes" })],
InboxItemListSchema,
EMPTY_INBOX_ITEMS,
ENDPOINT,
),
).toBe(EMPTY_INBOX_ITEMS);
});
});
describe("SearchProjectsResponseSchema date drift", () => {
const ENDPOINT = { endpoint: "GET /api/projects/search" };
const baseProject = {
id: "p-1",
workspace_id: "ws-1",
title: "Launch",
description: null,
icon: null,
status: "in_progress",
priority: "high",
lead_type: null,
lead_id: null,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
issue_count: 0,
done_count: 0,
resource_count: 0,
match_source: "title",
};
it("parses start_date / due_date when the backend returns them", () => {
const parsed = parseWithFallback(
{ projects: [{ ...baseProject, start_date: "2026-03-01", due_date: "2026-03-31" }], total: 1 },
SearchProjectsResponseSchema,
EMPTY_SEARCH_PROJECTS_RESPONSE,
ENDPOINT,
);
expect(parsed.projects[0]?.start_date).toBe("2026-03-01");
expect(parsed.projects[0]?.due_date).toBe("2026-03-31");
});
// Frontend deploys before backend: an older backend omits the new keys. The
// .default(null) must keep the whole batch parseable (→ null), not degrade
// it to the empty fallback and blank the search results.
it("defaults missing start_date / due_date to null without dropping results", () => {
const parsed = parseWithFallback(
{ projects: [baseProject], total: 1 },
SearchProjectsResponseSchema,
EMPTY_SEARCH_PROJECTS_RESPONSE,
ENDPOINT,
);
expect(parsed).not.toBe(EMPTY_SEARCH_PROJECTS_RESPONSE);
expect(parsed.projects).toHaveLength(1);
expect(parsed.projects[0]?.start_date).toBeNull();
expect(parsed.projects[0]?.due_date).toBeNull();
});
});
// The "run now" flow branches on run.status/reason_code to avoid a false-success
// toast (MUL-4525), so the trigger response must survive backend drift.
describe("AutopilotRunSchema", () => {
const ENDPOINT = { endpoint: "POST /api/autopilots/:id/trigger" };
const baseRun = {
id: "run-1",
autopilot_id: "ap-1",
trigger_id: null,
source: "manual",
status: "issue_created",
issue_id: "issue-1",
task_id: null,
triggered_at: "2026-07-14T00:00:00Z",
completed_at: null,
failure_reason: null,
trigger_payload: null,
result: null,
created_at: "2026-07-14T00:00:00Z",
};
it("preserves a blocked run's status and reason_code", () => {
const parsed = parseWithFallback(
{ ...baseRun, status: "skipped", failure_reason: "you are not allowed to trigger this autopilot's assignee agent", reason_code: "invocation_not_allowed" },
AutopilotRunSchema,
FALLBACK_AUTOPILOT_RUN,
ENDPOINT,
);
expect(parsed.status).toBe("skipped");
expect(parsed.reason_code).toBe("invocation_not_allowed");
});
it("tolerates an older server omitting reason_code", () => {
const parsed = parseWithFallback(baseRun, AutopilotRunSchema, FALLBACK_AUTOPILOT_RUN, ENDPOINT);
expect(parsed.status).toBe("issue_created");
expect(parsed.reason_code).toBeUndefined();
});
it("degrades a malformed response to a non-success fallback (never a false success)", () => {
const parsed = parseWithFallback("not-an-object", AutopilotRunSchema, FALLBACK_AUTOPILOT_RUN, ENDPOINT);
expect(parsed).toBe(FALLBACK_AUTOPILOT_RUN);
expect(parsed.status).toBe("failed");
});
});
// The comment composer branches on preview.blocked to warn before sending
// (MUL-4525 §2), so the additive field must parse and degrade gracefully.
describe("CommentTriggerPreviewSchema.blocked", () => {
it("parses blocked mention outcomes alongside agents", () => {
const parsed = CommentTriggerPreviewSchema.parse({
agents: [{ id: "a1", source: "mention_agent", reason: "" }],
blocked: [
{ target_type: "squad", target_id: "s1", status: "blocked", reason_code: "invocation_not_allowed" },
],
});
expect(parsed.agents).toHaveLength(1);
expect(parsed.blocked).toEqual([
{ target_type: "squad", target_id: "s1", status: "blocked", reason_code: "invocation_not_allowed" },
]);
});
it("defaults blocked to [] when an older server omits it", () => {
const parsed = CommentTriggerPreviewSchema.parse({ agents: [] });
expect(parsed.blocked).toEqual([]);
});
it("degrades a malformed blocked field to [] without dropping agents", () => {
const parsed = CommentTriggerPreviewSchema.parse({
agents: [{ id: "a1", source: "mention_agent", reason: "" }],
blocked: "nope",
});
expect(parsed.agents).toHaveLength(1);
expect(parsed.blocked).toEqual([]);
});
it("drops a single malformed blocked entry without discarding the valid ones", () => {
const parsed = CommentTriggerPreviewSchema.parse({
agents: [],
blocked: [
{ target_type: "squad", target_id: "s1", status: "blocked", reason_code: "invocation_not_allowed" },
{ status: "blocked" }, // missing target_id → dropped individually
{ target_type: "agent", target_id: "a1", status: "blocked", reason_code: "runtime_offline" },
],
});
expect(parsed.blocked.map((b) => b.target_id)).toEqual(["s1", "a1"]);
});
});
describe("RuntimeModelListRequestSchema", () => {
const completed = {
id: "req-1",
runtime_id: "rt-1",
status: "completed",
supported: true,
created_at: "2026-07-29T00:00:00Z",
updated_at: "2026-07-29T00:00:01Z",
models: [
{
id: "gpt-5.6-sol",
label: "GPT-5.6-Sol",
provider: "openai",
default: true,
thinking: {
supported_levels: [{ value: "high", label: "High" }],
default_level: "low",
},
service_tiers: [{ id: "fast", name: "Fast" }],
},
],
};
it("parses a live completed discovery, keeping the fields the UI branches on", () => {
const parsed = parseWithFallback(
completed,
RuntimeModelListRequestSchema,
MALFORMED_RUNTIME_MODEL_LIST_REQUEST,
{ endpoint: "test" },
);
expect(parsed.status).toBe("completed");
expect(parsed.supported).toBe(true);
expect(parsed.models?.[0]?.default).toBe(true);
expect(parsed.models?.[0]?.thinking?.supported_levels).toEqual([
{ value: "high", label: "High" },
]);
expect(parsed.models?.[0]?.service_tiers).toEqual([{ id: "fast", name: "Fast" }]);
expect(parsed.cached).toBeUndefined();
});
it("keeps the additive cache markers when the server serves a snapshot", () => {
const parsed = parseWithFallback(
{ ...completed, cached: true, cached_at: "2026-07-29T00:00:00Z" },
RuntimeModelListRequestSchema,
MALFORMED_RUNTIME_MODEL_LIST_REQUEST,
{ endpoint: "test" },
);
expect(parsed.cached).toBe(true);
expect(parsed.cached_at).toBe("2026-07-29T00:00:00Z");
});
// A backend that predates MUL-5444 sends neither marker; an even older one
// may omit `supported`. Both must stay usable rather than reading as
// "runtime manages the model itself" off an undefined.
it("defaults supported to true on an older backend that omits it", () => {
const { supported: _omitted, ...withoutSupported } = completed;
const parsed = parseWithFallback(
withoutSupported,
RuntimeModelListRequestSchema,
MALFORMED_RUNTIME_MODEL_LIST_REQUEST,
{ endpoint: "test" },
);
expect(parsed.supported).toBe(true);
expect(parsed.cached).toBeUndefined();
});
it("passes an unknown status through instead of failing the whole response", () => {
const parsed = parseWithFallback(
{ ...completed, status: "superseded" },
RuntimeModelListRequestSchema,
MALFORMED_RUNTIME_MODEL_LIST_REQUEST,
{ endpoint: "test" },
);
expect(parsed.status).toBe("superseded");
});
// Malformed bodies must land on the "failed" fallback: `completed` would
// fabricate an empty catalog and `pending` would spin the picker until the
// client-side poll timeout.
it("falls back to an explicit failure on a malformed body", () => {
for (const malformed of [
null,
"nope",
42,
{},
{ status: 7 },
{ ...completed, status: undefined },
{ ...completed, supported: "yes" },
{ ...completed, models: "nope" },
{ ...completed, models: [{ label: "no id" }] },
]) {
const parsed = parseWithFallback(
malformed,
RuntimeModelListRequestSchema,
MALFORMED_RUNTIME_MODEL_LIST_REQUEST,
{ endpoint: "test" },
);
expect(parsed.status).toBe("failed");
expect(parsed.supported).toBe(true);
expect(parsed.error).toBe("invalid model discovery response");
}
});
it("keeps unknown server fields instead of stripping them", () => {
const parsed = parseWithFallback(
{ ...completed, future_field: "keep me" },
RuntimeModelListRequestSchema,
MALFORMED_RUNTIME_MODEL_LIST_REQUEST,
{ endpoint: "test" },
);
expect((parsed as unknown as { future_field?: string }).future_field).toBe(
"keep me",
);
});
});