diff options
Diffstat (limited to 'packages/session-orchestrator/src/pure.test.ts')
| -rw-r--r-- | packages/session-orchestrator/src/pure.test.ts | 386 |
1 files changed, 245 insertions, 141 deletions
diff --git a/packages/session-orchestrator/src/pure.test.ts b/packages/session-orchestrator/src/pure.test.ts index 2cbe15f..d4fe84a 100644 --- a/packages/session-orchestrator/src/pure.test.ts +++ b/packages/session-orchestrator/src/pure.test.ts @@ -1,163 +1,267 @@ import type { ProviderContract } from "@dispatch/kernel"; import { describe, expect, it } from "vitest"; import { - buildUserMessage, - cumulativeSleepMs, - defaultDispatchPolicy, - delayFor, - generateTurnId, - RETRY_BUDGET_MS, - RETRY_SCHEDULE_MS, - RETRY_TAIL_MS, - resolveReasoningEffort, - selectFirstProvider, + buildUserMessage, + cumulativeSleepMs, + defaultDispatchPolicy, + delayFor, + generateTurnId, + type MemorySample, + memoryDelta, + memorySampleAttributes, + RETRY_BUDGET_MS, + RETRY_SCHEDULE_MS, + RETRY_TAIL_MS, + resolveReasoningEffort, + selectFirstProvider, } from "./pure.js"; describe("buildUserMessage", () => { - it("creates a user message with a single text chunk", () => { - const msg = buildUserMessage("hello world"); - expect(msg.role).toBe("user"); - expect(msg.chunks).toHaveLength(1); - expect(msg.chunks[0]).toEqual({ type: "text", text: "hello world" }); - }); - - it("preserves empty text", () => { - const msg = buildUserMessage(""); - expect(msg.role).toBe("user"); - expect(msg.chunks[0]).toEqual({ type: "text", text: "" }); - }); + it("creates a user message with a single text chunk", () => { + const msg = buildUserMessage("hello world"); + expect(msg.role).toBe("user"); + expect(msg.chunks).toHaveLength(1); + expect(msg.chunks[0]).toEqual({ type: "text", text: "hello world" }); + }); + + it("preserves empty text", () => { + const msg = buildUserMessage(""); + expect(msg.role).toBe("user"); + expect(msg.chunks[0]).toEqual({ type: "text", text: "" }); + }); + + it("appends image chunks after the text chunk when images are given", () => { + const msg = buildUserMessage("look at this", [ + { url: "data:image/png;base64,aaa" }, + { url: "data:image/jpeg;base64,bbb", mimeType: "image/jpeg" }, + ]); + expect(msg.chunks).toHaveLength(3); + expect(msg.chunks[0]).toEqual({ type: "text", text: "look at this" }); + expect(msg.chunks[1]).toEqual({ type: "image", url: "data:image/png;base64,aaa" }); + expect(msg.chunks[2]).toEqual({ + type: "image", + url: "data:image/jpeg;base64,bbb", + mimeType: "image/jpeg", + }); + }); + + it("builds an image-only message when text is empty", () => { + const msg = buildUserMessage("", [{ url: "data:image/png;base64,zzz" }]); + expect(msg.chunks).toHaveLength(1); + expect(msg.chunks[0]).toEqual({ type: "image", url: "data:image/png;base64,zzz" }); + }); + + it("includes mimeType when provided", () => { + const msg = buildUserMessage("hi", [ + { url: "data:image/webp;base64,x", mimeType: "image/webp" }, + ]); + expect((msg.chunks[1] as { mimeType?: string }).mimeType).toBe("image/webp"); + }); + + it("omits mimeType when not provided", () => { + const msg = buildUserMessage("hi", [{ url: "https://example.com/x.png" }]); + expect((msg.chunks[1] as { mimeType?: string }).mimeType).toBeUndefined(); + }); }); describe("selectFirstProvider", () => { - it("returns the first provider from a non-empty map", () => { - const provider: ProviderContract = { - id: "test-provider", - stream: async function* () {}, - }; - const providers = new Map<string, ProviderContract>(); - providers.set("test-provider", provider); - - expect(selectFirstProvider(providers)).toBe(provider); - }); - - it("throws when the map is empty", () => { - const providers = new Map<string, ProviderContract>(); - expect(() => selectFirstProvider(providers)).toThrow("No providers registered"); - }); - - it("returns the first inserted provider when multiple exist", () => { - const first: ProviderContract = { id: "first", stream: async function* () {} }; - const second: ProviderContract = { id: "second", stream: async function* () {} }; - const providers = new Map<string, ProviderContract>(); - providers.set("first", first); - providers.set("second", second); - - expect(selectFirstProvider(providers).id).toBe("first"); - }); + it("returns the first provider from a non-empty map", () => { + const provider: ProviderContract = { + id: "test-provider", + stream: async function* () {}, + }; + const providers = new Map<string, ProviderContract>(); + providers.set("test-provider", provider); + + expect(selectFirstProvider(providers)).toBe(provider); + }); + + it("throws when the map is empty", () => { + const providers = new Map<string, ProviderContract>(); + expect(() => selectFirstProvider(providers)).toThrow("No providers registered"); + }); + + it("returns the first inserted provider when multiple exist", () => { + const first: ProviderContract = { id: "first", stream: async function* () {} }; + const second: ProviderContract = { id: "second", stream: async function* () {} }; + const providers = new Map<string, ProviderContract>(); + providers.set("first", first); + providers.set("second", second); + + expect(selectFirstProvider(providers).id).toBe("first"); + }); }); describe("defaultDispatchPolicy", () => { - it("returns maxConcurrent: 1, eager: true", () => { - expect(defaultDispatchPolicy()).toEqual({ maxConcurrent: 1, eager: true }); - }); + it("returns maxConcurrent: 1, eager: true", () => { + expect(defaultDispatchPolicy()).toEqual({ maxConcurrent: 1, eager: true }); + }); }); describe("generateTurnId", () => { - it("returns a string starting with 'turn-'", () => { - const id = generateTurnId(); - expect(id).toMatch(/^turn-/); - }); - - it("returns unique ids", () => { - const ids = new Set(Array.from({ length: 100 }, () => generateTurnId())); - expect(ids.size).toBe(100); - }); + it("returns a string starting with 'turn-'", () => { + const id = generateTurnId(); + expect(id).toMatch(/^turn-/); + }); + + it("returns unique ids", () => { + const ids = new Set(Array.from({ length: 100 }, () => generateTurnId())); + expect(ids.size).toBe(100); + }); }); describe("resolveReasoningEffort", () => { - it("override wins over stored", () => { - expect(resolveReasoningEffort("low", "high")).toBe("low"); - expect(resolveReasoningEffort("max", "medium")).toBe("max"); - }); - - it("stored wins over default", () => { - expect(resolveReasoningEffort(undefined, "medium")).toBe("medium"); - expect(resolveReasoningEffort(undefined, "xhigh")).toBe("xhigh"); - }); - - it("default is 'high' when both are absent", () => { - expect(resolveReasoningEffort(undefined, null)).toBe("high"); - }); - - it("all 5 levels pass through as override", () => { - expect(resolveReasoningEffort("low", null)).toBe("low"); - expect(resolveReasoningEffort("medium", null)).toBe("medium"); - expect(resolveReasoningEffort("high", null)).toBe("high"); - expect(resolveReasoningEffort("xhigh", null)).toBe("xhigh"); - expect(resolveReasoningEffort("max", null)).toBe("max"); - }); - - it("all 5 levels pass through as stored", () => { - expect(resolveReasoningEffort(undefined, "low")).toBe("low"); - expect(resolveReasoningEffort(undefined, "medium")).toBe("medium"); - expect(resolveReasoningEffort(undefined, "high")).toBe("high"); - expect(resolveReasoningEffort(undefined, "xhigh")).toBe("xhigh"); - expect(resolveReasoningEffort(undefined, "max")).toBe("max"); - }); + it("override wins over stored", () => { + expect(resolveReasoningEffort("low", "high")).toBe("low"); + expect(resolveReasoningEffort("max", "medium")).toBe("max"); + }); + + it("stored wins over default", () => { + expect(resolveReasoningEffort(undefined, "medium")).toBe("medium"); + expect(resolveReasoningEffort(undefined, "xhigh")).toBe("xhigh"); + }); + + it("default is 'high' when both are absent", () => { + expect(resolveReasoningEffort(undefined, null)).toBe("high"); + }); + + it("all 5 levels pass through as override", () => { + expect(resolveReasoningEffort("low", null)).toBe("low"); + expect(resolveReasoningEffort("medium", null)).toBe("medium"); + expect(resolveReasoningEffort("high", null)).toBe("high"); + expect(resolveReasoningEffort("xhigh", null)).toBe("xhigh"); + expect(resolveReasoningEffort("max", null)).toBe("max"); + }); + + it("all 5 levels pass through as stored", () => { + expect(resolveReasoningEffort(undefined, "low")).toBe("low"); + expect(resolveReasoningEffort(undefined, "medium")).toBe("medium"); + expect(resolveReasoningEffort(undefined, "high")).toBe("high"); + expect(resolveReasoningEffort(undefined, "xhigh")).toBe("xhigh"); + expect(resolveReasoningEffort(undefined, "max")).toBe("max"); + }); }); describe("retry backoff schedule (delayFor)", () => { - it("emits the stepped head: 5s, 10s, 30s, 60s, 5m, 10m, 15m, 30m", () => { - expect(delayFor(0)).toBe(5_000); - expect(delayFor(1)).toBe(10_000); - expect(delayFor(2)).toBe(30_000); - expect(delayFor(3)).toBe(60_000); - expect(delayFor(4)).toBe(300_000); - expect(delayFor(5)).toBe(600_000); - expect(delayFor(6)).toBe(900_000); - expect(delayFor(7)).toBe(1_800_000); - }); - - it("repeats 30m after the head", () => { - expect(delayFor(8)).toBe(RETRY_TAIL_MS); - expect(delayFor(9)).toBe(RETRY_TAIL_MS); - expect(delayFor(20)).toBe(RETRY_TAIL_MS); - }); - - it("gives up (returns undefined) once cumulative sleep exceeds 8h", () => { - // Head sums to 3,705,000 ms; +1,800,000 per extra step. 8h = 28,800,000. - // attempt 20 cumulative = 3,705,000 + 13*1,800,000 = 27,105,000 (< 8h) → retry. - expect(delayFor(20)).toBe(RETRY_TAIL_MS); - // attempt 21 cumulative = 27,105,000 + 1,800,000 = 28,905,000 (> 8h) → stop. - expect(delayFor(21)).toBeUndefined(); - }); - - it("cumulativeSleepMs matches the sum of the schedule", () => { - expect(cumulativeSleepMs(0)).toBe(5_000); - expect(cumulativeSleepMs(1)).toBe(15_000); - expect(cumulativeSleepMs(7)).toBe(RETRY_SCHEDULE_MS.reduce((a, b) => a + b, 0)); - // 8h budget is 28,800,000 ms. - expect(RETRY_BUDGET_MS).toBe(8 * 60 * 60 * 1000); - // The last retry (attempt 20) keeps cumulative under budget. - expect(cumulativeSleepMs(20)).toBeLessThanOrEqual(RETRY_BUDGET_MS); - // The next (attempt 21) exceeds it. - expect(cumulativeSleepMs(21)).toBeGreaterThan(RETRY_BUDGET_MS); - }); - - it("the full schedule has 21 retries then stops", () => { - const schedule: number[] = []; - let attempt = 0; - while (true) { - const delay = delayFor(attempt); - if (delay === undefined) break; - schedule.push(delay); - attempt++; - } - expect(schedule).toHaveLength(21); - expect(schedule[0]).toBe(5_000); - expect(schedule.at(-1)).toBe(RETRY_TAIL_MS); - // 8 stepped head + 13 tail repeats. - expect(schedule.slice(0, 8)).toEqual([...RETRY_SCHEDULE_MS]); - expect(schedule.slice(8).every((d) => d === RETRY_TAIL_MS)).toBe(true); - }); + it("emits the stepped head: 5s, 10s, 30s, 60s, 5m, 10m, 15m, 30m", () => { + expect(delayFor(0)).toBe(5_000); + expect(delayFor(1)).toBe(10_000); + expect(delayFor(2)).toBe(30_000); + expect(delayFor(3)).toBe(60_000); + expect(delayFor(4)).toBe(300_000); + expect(delayFor(5)).toBe(600_000); + expect(delayFor(6)).toBe(900_000); + expect(delayFor(7)).toBe(1_800_000); + }); + + it("repeats 30m after the head", () => { + expect(delayFor(8)).toBe(RETRY_TAIL_MS); + expect(delayFor(9)).toBe(RETRY_TAIL_MS); + expect(delayFor(20)).toBe(RETRY_TAIL_MS); + }); + + it("gives up (returns undefined) once cumulative sleep exceeds 8h", () => { + // Head sums to 3,705,000 ms; +1,800,000 per extra step. 8h = 28,800,000. + // attempt 20 cumulative = 3,705,000 + 13*1,800,000 = 27,105,000 (< 8h) → retry. + expect(delayFor(20)).toBe(RETRY_TAIL_MS); + // attempt 21 cumulative = 27,105,000 + 1,800,000 = 28,905,000 (> 8h) → stop. + expect(delayFor(21)).toBeUndefined(); + }); + + it("cumulativeSleepMs matches the sum of the schedule", () => { + expect(cumulativeSleepMs(0)).toBe(5_000); + expect(cumulativeSleepMs(1)).toBe(15_000); + expect(cumulativeSleepMs(7)).toBe(RETRY_SCHEDULE_MS.reduce((a, b) => a + b, 0)); + // 8h budget is 28,800,000 ms. + expect(RETRY_BUDGET_MS).toBe(8 * 60 * 60 * 1000); + // The last retry (attempt 20) keeps cumulative under budget. + expect(cumulativeSleepMs(20)).toBeLessThanOrEqual(RETRY_BUDGET_MS); + // The next (attempt 21) exceeds it. + expect(cumulativeSleepMs(21)).toBeGreaterThan(RETRY_BUDGET_MS); + }); + + it("the full schedule has 21 retries then stops", () => { + const schedule: number[] = []; + let attempt = 0; + while (true) { + const delay = delayFor(attempt); + if (delay === undefined) break; + schedule.push(delay); + attempt++; + } + expect(schedule).toHaveLength(21); + expect(schedule[0]).toBe(5_000); + expect(schedule.at(-1)).toBe(RETRY_TAIL_MS); + // 8 stepped head + 13 tail repeats. + expect(schedule.slice(0, 8)).toEqual([...RETRY_SCHEDULE_MS]); + expect(schedule.slice(8).every((d) => d === RETRY_TAIL_MS)).toBe(true); + }); +}); + +describe("memorySampleAttributes", () => { + const sample: MemorySample = { + rss: 100 * 1024 * 1024, // 100 MB + heapUsed: 40 * 1024 * 1024, + heapTotal: 60 * 1024 * 1024, + external: 5 * 1024 * 1024, + arrayBuffers: 2 * 1024 * 1024, + }; + + it("formats fields as rounded MB with no prefix", () => { + const attrs = memorySampleAttributes(sample); + expect(attrs).toEqual({ + rssMB: 100, + heapUsedMB: 40, + heapTotalMB: 60, + externalMB: 5, + arrayBuffersMB: 2, + }); + }); + + it("namespaces keys with the given prefix", () => { + const attrs = memorySampleAttributes(sample, "delta"); + expect(attrs).toEqual({ + deltaRssMB: 100, + deltaHeapUsedMB: 40, + deltaHeapTotalMB: 60, + deltaExternalMB: 5, + deltaArrayBuffersMB: 2, + }); + }); + + it("rounds fractional MB", () => { + const attrs = memorySampleAttributes({ ...sample, rss: 100.6 * 1024 * 1024 }); + expect(attrs.rssMB).toBe(101); + }); +}); + +describe("memoryDelta", () => { + const before: MemorySample = { + rss: 200 * 1024 * 1024, + heapUsed: 100 * 1024 * 1024, + heapTotal: 150 * 1024 * 1024, + external: 10 * 1024 * 1024, + arrayBuffers: 4 * 1024 * 1024, + }; + const after: MemorySample = { + rss: 350 * 1024 * 1024, + heapUsed: 120 * 1024 * 1024, + heapTotal: 150 * 1024 * 1024, + external: 10 * 1024 * 1024, + arrayBuffers: 8 * 1024 * 1024, + }; + + it("computes signed after - before per field", () => { + const delta = memoryDelta(before, after); + expect(delta.rss).toBe(150 * 1024 * 1024); + expect(delta.heapUsed).toBe(20 * 1024 * 1024); + expect(delta.heapTotal).toBe(0); + expect(delta.external).toBe(0); + expect(delta.arrayBuffers).toBe(4 * 1024 * 1024); + }); + + it("is negative when memory dropped", () => { + const delta = memoryDelta(after, before); + expect(delta.rss).toBe(-150 * 1024 * 1024); + }); }); |
