diff options
| author | Adam Malczewski <[email protected]> | 2026-06-04 21:21:20 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-04 21:21:20 +0900 |
| commit | 394f1ed37ce860da6fdc385769bf29f9737105cd (patch) | |
| tree | 4b825dc642cb6eb9a060e54bf8d69288fbee4904 /packages/core/tests/models | |
| parent | 81a9cdbadf8c9d940d4fe9a2a0de607dee1f5f1a (diff) | |
| download | dispatch-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.ts | 136 | ||||
| -rw-r--r-- | packages/core/tests/models/catalog.test.ts | 227 |
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(); - }); -}); |
