summaryrefslogtreecommitdiffhomepage
path: root/packages/core/tests/models
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-04 21:21:20 +0900
committerAdam Malczewski <[email protected]>2026-06-04 21:21:20 +0900
commit394f1ed37ce860da6fdc385769bf29f9737105cd (patch)
tree4b825dc642cb6eb9a060e54bf8d69288fbee4904 /packages/core/tests/models
parent81a9cdbadf8c9d940d4fe9a2a0de607dee1f5f1a (diff)
downloaddispatch-394f1ed37ce860da6fdc385769bf29f9737105cd.tar.gz
dispatch-394f1ed37ce860da6fdc385769bf29f9737105cd.zip
chore: genesis — remove all files to rebuild from scratch (arch rewrite)
Diffstat (limited to 'packages/core/tests/models')
-rw-r--r--packages/core/tests/models/attachments.test.ts136
-rw-r--r--packages/core/tests/models/catalog.test.ts227
2 files changed, 0 insertions, 363 deletions
diff --git a/packages/core/tests/models/attachments.test.ts b/packages/core/tests/models/attachments.test.ts
deleted file mode 100644
index 11a9f82..0000000
--- a/packages/core/tests/models/attachments.test.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import { describe, expect, it } from "vitest";
-import {
- base64ByteLength,
- isAcceptedAttachmentMediaType,
- isImageMediaType,
- isPdfMediaType,
- MAX_ATTACHMENTS,
- MAX_IMAGE_BYTES,
- MAX_PDF_BYTES,
- MAX_TOTAL_ATTACHMENT_BYTES,
- validateUserContent,
-} from "../../src/models/attachments.js";
-import type { UserContentPart } from "../../src/types/index.js";
-
-/** A base64 string that decodes to exactly `bytes` bytes (no padding chars). */
-function base64OfBytes(bytes: number): string {
- // 4 base64 chars → 3 bytes. Use a multiple of 3 for clean (unpadded) output.
- const groups = Math.ceil(bytes / 3);
- return "A".repeat(groups * 4);
-}
-
-function imagePart(data: string, mediaType = "image/png"): UserContentPart {
- return { type: "attachment", mediaType, data };
-}
-
-describe("media-type predicates", () => {
- it("classifies image types", () => {
- expect(isImageMediaType("image/png")).toBe(true);
- expect(isImageMediaType("image/jpeg")).toBe(true);
- expect(isImageMediaType("image/webp")).toBe(true);
- expect(isImageMediaType("image/gif")).toBe(true);
- expect(isImageMediaType("application/pdf")).toBe(false);
- expect(isImageMediaType("image/svg+xml")).toBe(false);
- });
-
- it("classifies pdf + accepted types", () => {
- expect(isPdfMediaType("application/pdf")).toBe(true);
- expect(isPdfMediaType("image/png")).toBe(false);
- expect(isAcceptedAttachmentMediaType("image/gif")).toBe(true);
- expect(isAcceptedAttachmentMediaType("application/pdf")).toBe(true);
- expect(isAcceptedAttachmentMediaType("text/plain")).toBe(false);
- });
-});
-
-describe("base64ByteLength", () => {
- it("computes decoded length without padding", () => {
- // "AAAA" → 3 bytes.
- expect(base64ByteLength("AAAA")).toBe(3);
- });
-
- it("accounts for padding", () => {
- // "QQ==" → 1 byte ("A").
- expect(base64ByteLength("QQ==")).toBe(1);
- // "QUI=" → 2 bytes ("AB").
- expect(base64ByteLength("QUI=")).toBe(2);
- });
-
- it("tolerates a data: URI prefix and whitespace", () => {
- expect(base64ByteLength("data:image/png;base64,AAAA")).toBe(3);
- expect(base64ByteLength("AA\nAA")).toBe(3);
- });
-
- it("returns 0 for empty input", () => {
- expect(base64ByteLength("")).toBe(0);
- expect(base64ByteLength(" ")).toBe(0);
- });
-});
-
-describe("validateUserContent", () => {
- it("accepts a small image and ignores text parts", () => {
- const content: UserContentPart[] = [
- { type: "text", text: "hi" },
- imagePart(base64OfBytes(1024)),
- ];
- expect(validateUserContent(content)).toEqual({ ok: true, errors: [] });
- });
-
- it("accepts an empty / text-only content list", () => {
- expect(validateUserContent([]).ok).toBe(true);
- expect(validateUserContent([{ type: "text", text: "no files" }]).ok).toBe(true);
- });
-
- it("rejects an unsupported media type", () => {
- const res = validateUserContent([imagePart(base64OfBytes(10), "image/svg+xml")]);
- expect(res.ok).toBe(false);
- expect(res.errors[0]).toMatchObject({ code: "unsupported-type", mediaType: "image/svg+xml" });
- });
-
- it("rejects an oversized image but allows a PDF of the same size", () => {
- const big = base64OfBytes(MAX_IMAGE_BYTES + 3);
- const imgRes = validateUserContent([imagePart(big, "image/png")]);
- expect(imgRes.ok).toBe(false);
- expect(imgRes.errors.some((e) => e.code === "image-too-large")).toBe(true);
-
- // Same byte size as a PDF is fine (PDF limit is much higher).
- const pdfRes = validateUserContent([imagePart(big, "application/pdf")]);
- expect(pdfRes.ok).toBe(true);
- });
-
- it("rejects an oversized PDF", () => {
- const res = validateUserContent([
- imagePart(base64OfBytes(MAX_PDF_BYTES + 3), "application/pdf"),
- ]);
- expect(res.ok).toBe(false);
- expect(res.errors.some((e) => e.code === "pdf-too-large")).toBe(true);
- });
-
- it("rejects an empty attachment payload", () => {
- const res = validateUserContent([imagePart("", "image/png")]);
- expect(res.ok).toBe(false);
- expect(res.errors.some((e) => e.code === "empty")).toBe(true);
- });
-
- it("rejects too many attachments", () => {
- const content: UserContentPart[] = Array.from({ length: MAX_ATTACHMENTS + 1 }, () =>
- imagePart(base64OfBytes(8)),
- );
- const res = validateUserContent(content);
- expect(res.ok).toBe(false);
- expect(res.errors.some((e) => e.code === "too-many")).toBe(true);
- });
-
- it("rejects when the total payload exceeds the request ceiling", () => {
- // Several individually-legal PDFs that together exceed the total cap.
- const each = Math.floor(MAX_TOTAL_ATTACHMENT_BYTES / 3);
- const content: UserContentPart[] = [
- imagePart(base64OfBytes(each), "application/pdf"),
- imagePart(base64OfBytes(each), "application/pdf"),
- imagePart(base64OfBytes(each), "application/pdf"),
- imagePart(base64OfBytes(each), "application/pdf"),
- ];
- const res = validateUserContent(content);
- expect(res.ok).toBe(false);
- expect(res.errors.some((e) => e.code === "total-too-large")).toBe(true);
- });
-});
diff --git a/packages/core/tests/models/catalog.test.ts b/packages/core/tests/models/catalog.test.ts
deleted file mode 100644
index f4bddc2..0000000
--- a/packages/core/tests/models/catalog.test.ts
+++ /dev/null
@@ -1,227 +0,0 @@
-import { existsSync, rmSync, utimesSync, writeFileSync } from "node:fs";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import {
- __resetCatalogCacheForTests,
- getModelsCatalog,
- resolveContextLimit,
- resolveModelCapabilities,
-} from "../../src/models/catalog.js";
-
-const CACHE_PATH = "/tmp/dispatch/models-dev.json";
-
-// A trimmed models.dev-shaped catalog covering the providers we support.
-const CATALOG = {
- anthropic: {
- id: "anthropic",
- models: {
- "claude-sonnet-4-5": {
- limit: { context: 200000, output: 64000 },
- modalities: { input: ["text", "image", "pdf"], output: ["text"] },
- },
- "claude-sonnet-4-6": {
- limit: { context: 1000000, output: 64000 },
- modalities: { input: ["text", "image", "pdf"], output: ["text"] },
- },
- // A text-only model: definitively no image/pdf input.
- "text-only-model": {
- limit: { context: 100000, output: 8192 },
- modalities: { input: ["text"], output: ["text"] },
- },
- // An entry predating the modalities field → capability unknown.
- "legacy-model": { limit: { context: 100000, output: 8192 } },
- },
- },
- opencode: {
- id: "opencode",
- models: {
- "glm-4-6": {
- limit: { context: 131072, output: 8192 },
- modalities: { input: ["text", "image"], output: ["text"] },
- },
- },
- },
-};
-
-function mockFetchOnce(catalog: unknown, ok = true, status = 200) {
- const fn = vi.fn(() =>
- Promise.resolve({
- ok,
- status,
- text: () => Promise.resolve(JSON.stringify(catalog)),
- } as Response),
- );
- vi.stubGlobal("fetch", fn);
- return fn;
-}
-
-beforeEach(() => {
- __resetCatalogCacheForTests();
- if (existsSync(CACHE_PATH)) rmSync(CACHE_PATH);
- delete process.env.DISPATCH_DISABLE_MODELS_FETCH;
-});
-
-afterEach(() => {
- vi.unstubAllGlobals();
- if (existsSync(CACHE_PATH)) rmSync(CACHE_PATH);
-});
-
-describe("resolveContextLimit", () => {
- it("resolves a known anthropic model to its context window", async () => {
- mockFetchOnce(CATALOG);
- expect(await resolveContextLimit("anthropic", "claude-sonnet-4-5")).toBe(200000);
- expect(await resolveContextLimit("anthropic", "claude-sonnet-4-6")).toBe(1000000);
- });
-
- it("maps opencode-anthropic to the anthropic catalog, then opencode fallback", async () => {
- mockFetchOnce(CATALOG);
- // Present in the anthropic catalog.
- expect(await resolveContextLimit("opencode-anthropic", "claude-sonnet-4-5")).toBe(200000);
- // Absent in anthropic, found in the opencode gateway catalog.
- expect(await resolveContextLimit("opencode-anthropic", "glm-4-6")).toBe(131072);
- });
-
- it("returns null for an unknown model id", async () => {
- mockFetchOnce(CATALOG);
- expect(await resolveContextLimit("anthropic", "no-such-model")).toBeNull();
- });
-
- it("returns null for an unsupported provider (no network needed)", async () => {
- const fetchFn = mockFetchOnce(CATALOG);
- expect(await resolveContextLimit("google", "gemini-2.5-pro")).toBeNull();
- expect(await resolveContextLimit("anthropic", "")).toBeNull();
- expect(fetchFn).not.toHaveBeenCalled();
- });
-
- it("returns null when the model has no positive context limit", async () => {
- mockFetchOnce({
- anthropic: { id: "anthropic", models: { broken: { limit: { context: 0 } } } },
- });
- expect(await resolveContextLimit("anthropic", "broken")).toBeNull();
- });
-
- it("does not throw on a malformed provider entry missing `models`", async () => {
- // A provider object without a `models` map must degrade to null, not crash.
- mockFetchOnce({ anthropic: { id: "anthropic" } });
- expect(await resolveContextLimit("anthropic", "claude-sonnet-4-5")).toBeNull();
- });
-
- it("does not throw when limit/context fields are absent", async () => {
- mockFetchOnce({ anthropic: { id: "anthropic", models: { m: {} } } });
- expect(await resolveContextLimit("anthropic", "m")).toBeNull();
- });
-});
-
-describe("getModelsCatalog caching", () => {
- it("fetches once and serves the in-process memo on subsequent calls", async () => {
- const fetchFn = mockFetchOnce(CATALOG);
- await resolveContextLimit("anthropic", "claude-sonnet-4-5");
- await resolveContextLimit("anthropic", "claude-sonnet-4-6");
- await getModelsCatalog();
- expect(fetchFn).toHaveBeenCalledTimes(1);
- });
-
- it("reuses a fresh disk cache without re-fetching across processes", async () => {
- // Simulate another process having written a fresh cache.
- writeFileSync(CACHE_PATH, JSON.stringify(CATALOG), "utf-8");
- const fetchFn = vi.fn(() => Promise.reject(new Error("network should not be hit")));
- vi.stubGlobal("fetch", fetchFn);
- expect(await resolveContextLimit("anthropic", "claude-sonnet-4-5")).toBe(200000);
- expect(fetchFn).not.toHaveBeenCalled();
- });
-
- it("falls back to a STALE disk cache when the network fails", async () => {
- writeFileSync(CACHE_PATH, JSON.stringify(CATALOG), "utf-8");
- // Age the cache well past the TTL so the fetch path is taken.
- const old = Date.now() / 1000 - 3600;
- utimesSync(CACHE_PATH, old, old);
- const fetchFn = vi.fn(() => Promise.reject(new Error("offline")));
- vi.stubGlobal("fetch", fetchFn);
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
-
- expect(await resolveContextLimit("anthropic", "claude-sonnet-4-5")).toBe(200000);
- expect(fetchFn).toHaveBeenCalledTimes(1);
- warn.mockRestore();
- });
-
- it("returns null when fetch fails and no cache exists", async () => {
- const fetchFn = vi.fn(() => Promise.reject(new Error("offline")));
- vi.stubGlobal("fetch", fetchFn);
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
- expect(await resolveContextLimit("anthropic", "claude-sonnet-4-5")).toBeNull();
- warn.mockRestore();
- });
-
- it("does not hit the network when DISPATCH_DISABLE_MODELS_FETCH is set", async () => {
- process.env.DISPATCH_DISABLE_MODELS_FETCH = "1";
- const fetchFn = vi.fn(() => Promise.reject(new Error("should not fetch")));
- vi.stubGlobal("fetch", fetchFn);
- expect(await resolveContextLimit("anthropic", "claude-sonnet-4-5")).toBeNull();
- expect(fetchFn).not.toHaveBeenCalled();
- });
-
- it("memoizes the fallback after a failed fetch so it does not re-hit the network", async () => {
- const fetchFn = vi.fn(() => Promise.reject(new Error("offline")));
- vi.stubGlobal("fetch", fetchFn);
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
-
- // First lookup triggers the (failing) fetch.
- expect(await resolveContextLimit("anthropic", "claude-sonnet-4-5")).toBeNull();
- // Subsequent lookups within the penalty window must NOT re-fetch.
- expect(await resolveContextLimit("anthropic", "claude-sonnet-4-6")).toBeNull();
- await getModelsCatalog();
- expect(fetchFn).toHaveBeenCalledTimes(1);
- warn.mockRestore();
- });
-});
-
-describe("resolveModelCapabilities", () => {
- it("reports image + pdf for a vision model", async () => {
- mockFetchOnce(CATALOG);
- expect(await resolveModelCapabilities("anthropic", "claude-sonnet-4-5")).toEqual({
- image: true,
- pdf: true,
- });
- });
-
- it("reports image-only for a model whose modalities omit pdf", async () => {
- mockFetchOnce(CATALOG);
- // glm-4-6 lists image but not pdf (resolved via the opencode fallback).
- expect(await resolveModelCapabilities("opencode-anthropic", "glm-4-6")).toEqual({
- image: true,
- pdf: false,
- });
- });
-
- it("reports a definitive no for a text-only model", async () => {
- mockFetchOnce(CATALOG);
- expect(await resolveModelCapabilities("anthropic", "text-only-model")).toEqual({
- image: false,
- pdf: false,
- });
- });
-
- it("returns null (unknown) for an entry without modalities", async () => {
- mockFetchOnce(CATALOG);
- expect(await resolveModelCapabilities("anthropic", "legacy-model")).toBeNull();
- });
-
- it("returns null (unknown) for an unknown model id", async () => {
- mockFetchOnce(CATALOG);
- expect(await resolveModelCapabilities("anthropic", "no-such-model")).toBeNull();
- });
-
- it("returns null for an unsupported provider without hitting the network", async () => {
- const fetchFn = mockFetchOnce(CATALOG);
- expect(await resolveModelCapabilities("google", "gemini-2.5-pro")).toBeNull();
- expect(await resolveModelCapabilities("anthropic", "")).toBeNull();
- expect(fetchFn).not.toHaveBeenCalled();
- });
-
- it("returns null (unknown) when the catalog is offline with no cache", async () => {
- const fetchFn = vi.fn(() => Promise.reject(new Error("offline")));
- vi.stubGlobal("fetch", fetchFn);
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
- expect(await resolveModelCapabilities("anthropic", "claude-sonnet-4-5")).toBeNull();
- warn.mockRestore();
- });
-});