Files
multica/packages/core/api/schemas.test.ts
Bohan Jiang 6c17771cce fix: re-sign inline attachment media for token-mode clients (#4085)
The two prior MUL-3254 fixes preserved draft/description state across a
modal close, but Desktop still could not RENDER the reopened image: in
CloudFront signed-URL mode every URL the renderer holds after reopen is
unloadable. The persisted record strips the expired signed download_url,
the raw CDN url is unsigned (403 on a signed distribution), and the
durable /api/attachments/<id>/download endpoint needs credentials that a
cross-site file:// <img> fetch cannot carry (web works via the same-site
session cookie, which is why the bug was desktop-only).

Two changes close the last mile:

- /api/config now reports cdn_signed when CloudFront signing is enabled,
  and pickInlineMediaURL stops picking the raw (unsigned) CDN url in
  that mode — it is a guaranteed 403.
- The Attachment renderer upgrades an auth-gated media URL to a freshly
  signed one via authenticated GET /api/attachments/<id> (the same
  re-sign the click-time download path already does), but only on
  clients without a same-origin /api proxy (api.getBaseUrl() non-empty:
  Desktop, mobile webview). Cached via TanStack Query with a 20-minute
  staleTime, inside the server's 30-minute signed-URL TTL.

Old servers omit cdn_signed; the schema defaults it to false so behavior
is unchanged there. Non-CloudFront deployments return the API path again
from the metadata fetch and the renderer keeps the original URL.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-15 13:54:36 +08:00

295 lines
10 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,
DashboardAgentRunTimeListSchema,
DashboardUsageByAgentListSchema,
DashboardUsageDailyListSchema,
DuplicateIssueErrorBodySchema,
EMPTY_USER,
ListIssuesResponseSchema,
RuntimeHourlyActivityListSchema,
RuntimeUsageByAgentListSchema,
RuntimeUsageByHourListSchema,
RuntimeUsageListSchema,
SquadListSchema,
SquadSchema,
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,
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);
});
});
// 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("rejects a non-array body so parseWithFallback can return its fallback", () => {
expect(DashboardUsageDailyListSchema.safeParse(null).success).toBe(false);
expect(RuntimeUsageListSchema.safeParse({ rows: [] }).success).toBe(false);
});
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);
});
});