From 06e959e7a5c4ae83b020b371f68793a316ad6c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Thu, 4 Jun 2026 04:35:33 +0000 Subject: [PATCH] test: add unit tests for core modules (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover high-priority untested modules: - util: base32, result, refs-field, storage-root, log-tag - util-agent: storage (normalizeWorkflowConfig, resolveStorageRoot), run (parseArgv) - agent-builtin: tools (read-file, write-file, run-command), session, detail 627 → 719 tests (+92), all passing. Refs #35 --- package.json | 1 + .../agent-builtin/__tests__/detail.test.ts | 49 ++++++ .../agent-builtin/__tests__/read-file.test.ts | 51 ++++++ .../__tests__/run-command.test.ts | 38 ++++ .../agent-builtin/__tests__/session.test.ts | 65 +++++++ .../__tests__/write-file.test.ts | 43 +++++ .../__tests__/run-parseArgv.test.ts | 38 ++++ packages/util-agent/__tests__/storage.test.ts | 165 ++++++++++++++++++ packages/util/__tests__/base32.test.ts | 130 ++++++++++++++ packages/util/__tests__/log-tag.test.ts | 38 ++++ packages/util/__tests__/refs-field.test.ts | 40 +++++ packages/util/__tests__/result.test.ts | 51 ++++++ packages/util/__tests__/storage-root.test.ts | 25 +++ pnpm-lock.yaml | 139 +++++++++++++++ 14 files changed, 873 insertions(+) create mode 100644 packages/agent-builtin/__tests__/detail.test.ts create mode 100644 packages/agent-builtin/__tests__/read-file.test.ts create mode 100644 packages/agent-builtin/__tests__/run-command.test.ts create mode 100644 packages/agent-builtin/__tests__/session.test.ts create mode 100644 packages/agent-builtin/__tests__/write-file.test.ts create mode 100644 packages/util-agent/__tests__/run-parseArgv.test.ts create mode 100644 packages/util-agent/__tests__/storage.test.ts create mode 100644 packages/util/__tests__/base32.test.ts create mode 100644 packages/util/__tests__/log-tag.test.ts create mode 100644 packages/util/__tests__/refs-field.test.ts create mode 100644 packages/util/__tests__/result.test.ts create mode 100644 packages/util/__tests__/storage-root.test.ts diff --git a/package.json b/package.json index 13c6fb1..79b93ce 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@types/node": "^25.7.0", "@types/xxhashjs": "^0.2.4", "@united-workforce/agent-hermes": "workspace:*", + "@vitest/coverage-v8": "^4.1.8", "typescript": "^5.8.3", "vitest": "^3.2.1", "yaml": "^2.9.0" diff --git a/packages/agent-builtin/__tests__/detail.test.ts b/packages/agent-builtin/__tests__/detail.test.ts new file mode 100644 index 0000000..bf91d32 --- /dev/null +++ b/packages/agent-builtin/__tests__/detail.test.ts @@ -0,0 +1,49 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { createMemoryStore } from "@ocas/core"; +import { storeBuiltinDetail } from "../src/detail.js"; +import { appendSessionTurn, initSessionDir } from "../src/session.js"; +import type { BuiltinTurnPayload } from "../src/types.js"; + +describe("storeBuiltinDetail", () => { + let storageRoot: string; + + beforeEach(async () => { + storageRoot = await mkdtemp(join(tmpdir(), "builtin-detail-storage-")); + }); + + afterEach(async () => { + await rm(storageRoot, { recursive: true, force: true }); + }); + + const makeTurn = (role: "assistant" | "tool", content: string): BuiltinTurnPayload => ({ + role, + content, + toolCalls: null, + reasoning: null, + }); + + test("stores detail with turns, returns hash and turnCount", async () => { + const store = createMemoryStore(); + await initSessionDir(storageRoot); + const sid = "detail-test"; + await appendSessionTurn(storageRoot, sid, makeTurn("tool", "question")); + await appendSessionTurn(storageRoot, sid, makeTurn("assistant", "answer")); + + const result = await storeBuiltinDetail(store, storageRoot, sid, "test-model", 1000, 2000); + expect(result.turnCount).toBe(2); + expect(typeof result.detailHash).toBe("string"); + expect(result.detailHash.length).toBeGreaterThan(0); + }); + + test("empty session returns turnCount 0", async () => { + const store = createMemoryStore(); + const sid = "empty-session"; + + const result = await storeBuiltinDetail(store, storageRoot, sid, "test-model", 1000, 2000); + expect(result.turnCount).toBe(0); + expect(typeof result.detailHash).toBe("string"); + }); +}); diff --git a/packages/agent-builtin/__tests__/read-file.test.ts b/packages/agent-builtin/__tests__/read-file.test.ts new file mode 100644 index 0000000..aae8ef9 --- /dev/null +++ b/packages/agent-builtin/__tests__/read-file.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { readFileTool } from "../src/tools/read-file.js"; +import { writeFile, mkdir, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +const testDir = join(tmpdir(), `read-file-test-${Date.now()}`); +const ctx = { cwd: testDir, storageRoot: testDir }; + +beforeAll(async () => { + await mkdir(testDir, { recursive: true }); + await writeFile(join(testDir, "hello.txt"), "hello world", "utf8"); +}); + +afterAll(async () => { + await rm(testDir, { recursive: true, force: true }); +}); + +describe("readFileTool", () => { + it("reads a file successfully", async () => { + const result = await readFileTool.execute({ path: "hello.txt" }, ctx); + expect(result).toBe("hello world"); + }); + + it("returns error for non-existent file", async () => { + const result = await readFileTool.execute({ path: "nope.txt" }, ctx); + expect(result).toMatch(/^Error:/); + }); + + it("returns error for directory", async () => { + const result = await readFileTool.execute({ path: "." }, ctx); + expect(result).toBe("Error: not a file"); + }); + + it("returns error when path is not a string", async () => { + const result = await readFileTool.execute({ path: 123 }, ctx); + expect(result).toBe("Error: path must be a string"); + }); + + it("returns error when args is null", async () => { + const result = await readFileTool.execute(null, ctx); + expect(result).toBe("Error: path must be a string"); + }); + + it("returns error for file exceeding 512KB limit", async () => { + const bigFile = join(testDir, "big.txt"); + await writeFile(bigFile, Buffer.alloc(512 * 1024 + 1, 65)); + const result = await readFileTool.execute({ path: "big.txt" }, ctx); + expect(result).toMatch(/Error:.*limit/); + }); +}); diff --git a/packages/agent-builtin/__tests__/run-command.test.ts b/packages/agent-builtin/__tests__/run-command.test.ts new file mode 100644 index 0000000..7573063 --- /dev/null +++ b/packages/agent-builtin/__tests__/run-command.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { runCommandTool } from "../src/tools/run-command.js"; +import { tmpdir } from "node:os"; + +const ctx = { cwd: tmpdir(), storageRoot: tmpdir() }; + +describe("runCommandTool", () => { + it("runs echo command and checks stdout", async () => { + const result = await runCommandTool.execute({ command: "echo hello" }, ctx); + expect(result).toContain("hello"); + expect(result).toContain("stdout"); + }); + + it("returns exit code", async () => { + const result = await runCommandTool.execute({ command: "exit 0" }, ctx); + expect(result).toContain("exit_code: 0"); + }); + + it("returns non-zero exit code", async () => { + const result = await runCommandTool.execute({ command: "exit 42" }, ctx); + expect(result).toContain("exit_code: 42"); + }); + + it("returns error when command is not a string", async () => { + const result = await runCommandTool.execute({ command: 123 }, ctx); + expect(result).toBe("Error: command must be a string"); + }); + + it("returns error when args is null", async () => { + const result = await runCommandTool.execute(null, ctx); + expect(result).toBe("Error: command must be a string"); + }); + + it("custom cwd works", async () => { + const result = await runCommandTool.execute({ command: "pwd", cwd: "/tmp" }, ctx); + expect(result).toContain("/tmp"); + }); +}); diff --git a/packages/agent-builtin/__tests__/session.test.ts b/packages/agent-builtin/__tests__/session.test.ts new file mode 100644 index 0000000..e32c741 --- /dev/null +++ b/packages/agent-builtin/__tests__/session.test.ts @@ -0,0 +1,65 @@ +import { existsSync } from "node:fs"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import type { BuiltinTurnPayload } from "../src/types.js"; +import { + appendSessionTurn, + initSessionDir, + readSessionTurns, + removeSession, +} from "../src/session.js"; + +describe("session", () => { + let storageRoot: string; + + beforeEach(async () => { + storageRoot = await mkdtemp(join(tmpdir(), "builtin-session-")); + }); + + afterEach(async () => { + await rm(storageRoot, { recursive: true, force: true }); + }); + + const makeTurn = (role: "assistant" | "tool", content: string): BuiltinTurnPayload => ({ + role, + content, + toolCalls: null, + reasoning: null, + }); + + test("initSessionDir creates directory", async () => { + await initSessionDir(storageRoot); + expect(existsSync(join(storageRoot, "sessions"))).toBe(true); + }); + + test("append + read roundtrip", async () => { + await initSessionDir(storageRoot); + const sid = "test-session-1"; + const t1 = makeTurn("tool", "hello"); + const t2 = makeTurn("assistant", "hi there"); + await appendSessionTurn(storageRoot, sid, t1); + await appendSessionTurn(storageRoot, sid, t2); + const turns = await readSessionTurns(storageRoot, sid); + expect(turns).toEqual([t1, t2]); + }); + + test("read from non-existent returns []", async () => { + const turns = await readSessionTurns(storageRoot, "no-such-session"); + expect(turns).toEqual([]); + }); + + test("removeSession deletes file", async () => { + await initSessionDir(storageRoot); + const sid = "to-remove"; + await appendSessionTurn(storageRoot, sid, makeTurn("tool", "bye")); + await removeSession(storageRoot, sid); + const turns = await readSessionTurns(storageRoot, sid); + expect(turns).toEqual([]); + }); + + test("removeSession on non-existent does not throw", async () => { + await expect(removeSession(storageRoot, "ghost")).resolves.toBeUndefined(); + }); +}); diff --git a/packages/agent-builtin/__tests__/write-file.test.ts b/packages/agent-builtin/__tests__/write-file.test.ts new file mode 100644 index 0000000..0b064f2 --- /dev/null +++ b/packages/agent-builtin/__tests__/write-file.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect, afterAll } from "vitest"; +import { writeFileTool } from "../src/tools/write-file.js"; +import { readFile, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +const testDir = join(tmpdir(), `write-file-test-${Date.now()}`); +const ctx = { cwd: testDir, storageRoot: testDir }; + +afterAll(async () => { + await rm(testDir, { recursive: true, force: true }); +}); + +describe("writeFileTool", () => { + it("writes file successfully", async () => { + const result = await writeFileTool.execute({ path: "out.txt", content: "hi" }, ctx); + expect(result).toMatch(/Wrote 2 bytes/); + const content = await readFile(join(testDir, "out.txt"), "utf8"); + expect(content).toBe("hi"); + }); + + it("creates parent directories", async () => { + const result = await writeFileTool.execute({ path: "a/b/c.txt", content: "nested" }, ctx); + expect(result).toMatch(/Wrote/); + const content = await readFile(join(testDir, "a/b/c.txt"), "utf8"); + expect(content).toBe("nested"); + }); + + it("returns error when path is not a string", async () => { + const result = await writeFileTool.execute({ path: 123, content: "x" }, ctx); + expect(result).toBe("Error: path and content must be strings"); + }); + + it("returns error when content is not a string", async () => { + const result = await writeFileTool.execute({ path: "x.txt", content: 42 }, ctx); + expect(result).toBe("Error: path and content must be strings"); + }); + + it("returns error when args is null", async () => { + const result = await writeFileTool.execute(null, ctx); + expect(result).toBe("Error: path and content must be strings"); + }); +}); diff --git a/packages/util-agent/__tests__/run-parseArgv.test.ts b/packages/util-agent/__tests__/run-parseArgv.test.ts new file mode 100644 index 0000000..0bad04e --- /dev/null +++ b/packages/util-agent/__tests__/run-parseArgv.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { parseArgv } from "../src/run.js"; + +describe("parseArgv", () => { + let exitSpy: ReturnType; + let stderrSpy: ReturnType; + + beforeEach(() => { + exitSpy = vi.spyOn(process, "exit").mockImplementation((() => { + throw new Error("process.exit"); + }) as never); + stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation((() => true) as never); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("returns threadId, role, prompt for valid argv", () => { + const result = parseArgv(["node", "script", "--thread", "abc123", "--role", "developer", "--prompt", "do stuff"]); + expect(result).toEqual({ threadId: "abc123", role: "developer", prompt: "do stuff" }); + }); + + it("exits when --thread is missing", () => { + expect(() => parseArgv(["node", "script", "--role", "dev", "--prompt", "x"])).toThrow("process.exit"); + expect(exitSpy).toHaveBeenCalledWith(1); + }); + + it("exits when --role is missing", () => { + expect(() => parseArgv(["node", "script", "--thread", "t1", "--prompt", "x"])).toThrow("process.exit"); + expect(exitSpy).toHaveBeenCalledWith(1); + }); + + it("exits when --prompt is missing", () => { + expect(() => parseArgv(["node", "script", "--thread", "t1", "--role", "dev"])).toThrow("process.exit"); + expect(exitSpy).toHaveBeenCalledWith(1); + }); +}); diff --git a/packages/util-agent/__tests__/storage.test.ts b/packages/util-agent/__tests__/storage.test.ts new file mode 100644 index 0000000..1cede7a --- /dev/null +++ b/packages/util-agent/__tests__/storage.test.ts @@ -0,0 +1,165 @@ +import { homedir } from "node:os"; +import { join } from "node:path"; +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { + resolveStorageRoot, + getDefaultStorageRoot, + getCasDir, + getConfigPath, + getEnvPath, + getGlobalCasDir, + normalizeWorkflowConfig, +} from "../src/storage.js"; + +const VALID_CONFIG = { + defaultAgent: "builtin", + defaultModel: "main", + providers: { openai: { baseUrl: "https://api.openai.com/v1", apiKey: "sk-xxx" } }, + models: { main: { provider: "openai", name: "gpt-4" } }, + agents: { builtin: { command: "uwf-builtin", args: ["--verbose"] } }, +}; + +describe("getDefaultStorageRoot", () => { + it("returns homedir/.uwf", () => { + expect(getDefaultStorageRoot()).toBe(join(homedir(), ".uwf")); + }); +}); + +describe("resolveStorageRoot", () => { + const saved: Record = {}; + + beforeEach(() => { + saved.UWF_STORAGE_ROOT = process.env.UWF_STORAGE_ROOT; + saved.WORKFLOW_STORAGE_ROOT = process.env.WORKFLOW_STORAGE_ROOT; + }); + + afterEach(() => { + for (const k of ["UWF_STORAGE_ROOT", "WORKFLOW_STORAGE_ROOT"] as const) { + if (saved[k] === undefined) delete process.env[k]; + else process.env[k] = saved[k]; + } + }); + + it("uses UWF_STORAGE_ROOT first", () => { + process.env.UWF_STORAGE_ROOT = "/tmp/uwf1"; + process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf2"; + expect(resolveStorageRoot()).toBe("/tmp/uwf1"); + }); + + it("falls back to WORKFLOW_STORAGE_ROOT", () => { + delete process.env.UWF_STORAGE_ROOT; + process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf2"; + expect(resolveStorageRoot()).toBe("/tmp/uwf2"); + }); + + it("falls back to default when both unset", () => { + delete process.env.UWF_STORAGE_ROOT; + delete process.env.WORKFLOW_STORAGE_ROOT; + expect(resolveStorageRoot()).toBe(getDefaultStorageRoot()); + }); + + it("ignores empty UWF_STORAGE_ROOT", () => { + process.env.UWF_STORAGE_ROOT = ""; + process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf2"; + expect(resolveStorageRoot()).toBe("/tmp/uwf2"); + }); +}); + +describe("path helpers", () => { + it("getCasDir", () => expect(getCasDir("/root")).toBe("/root/cas")); + it("getConfigPath", () => expect(getConfigPath("/root")).toBe("/root/config.yaml")); + it("getEnvPath", () => expect(getEnvPath("/root")).toBe("/root/.env")); +}); + +describe("getGlobalCasDir", () => { + const saved = { OCAS_DIR: process.env.OCAS_DIR }; + + afterEach(() => { + if (saved.OCAS_DIR === undefined) delete process.env.OCAS_DIR; + else process.env.OCAS_DIR = saved.OCAS_DIR; + }); + + it("uses OCAS_DIR when set", () => { + process.env.OCAS_DIR = "/tmp/ocas"; + expect(getGlobalCasDir()).toBe("/tmp/ocas"); + }); + + it("defaults to ~/.ocas", () => { + delete process.env.OCAS_DIR; + expect(getGlobalCasDir()).toBe(join(homedir(), ".ocas")); + }); +}); + +describe("normalizeWorkflowConfig", () => { + it("normalizes a valid config", () => { + const result = normalizeWorkflowConfig(VALID_CONFIG); + expect(result.defaultAgent).toBe("builtin"); + expect(result.defaultModel).toBe("main"); + expect(result.providers.openai.baseUrl).toBe("https://api.openai.com/v1"); + expect(result.models.main.name).toBe("gpt-4"); + expect(result.agents.builtin.command).toBe("uwf-builtin"); + expect(result.agents.builtin.args).toEqual(["--verbose"]); + expect(result.modelOverrides).toBeNull(); + expect(result.agentOverrides).toBeNull(); + }); + + it("throws on non-record root", () => { + expect(() => normalizeWorkflowConfig("bad")).toThrow("root must be a mapping"); + expect(() => normalizeWorkflowConfig(null)).toThrow("root must be a mapping"); + expect(() => normalizeWorkflowConfig([])).toThrow("root must be a mapping"); + }); + + it("throws when defaultAgent missing", () => { + expect(() => normalizeWorkflowConfig({ ...VALID_CONFIG, defaultAgent: undefined })) + .toThrow("defaultAgent and defaultModel"); + }); + + it("throws when defaultModel missing", () => { + expect(() => normalizeWorkflowConfig({ ...VALID_CONFIG, defaultModel: 42 })) + .toThrow("defaultAgent and defaultModel"); + }); + + it("throws on invalid providers entry", () => { + expect(() => normalizeWorkflowConfig({ ...VALID_CONFIG, providers: { bad: "string" } })) + .toThrow("config.providers.bad must be a mapping"); + }); + + it("throws on invalid models entry", () => { + expect(() => normalizeWorkflowConfig({ ...VALID_CONFIG, models: { m: { provider: 123, name: "x" } } })) + .toThrow("config.models.m requires provider and name"); + }); + + it("throws on invalid agents entry", () => { + expect(() => normalizeWorkflowConfig({ ...VALID_CONFIG, agents: "bad" })) + .toThrow("config.agents must be a mapping"); + }); + + it("returns null for undefined modelOverrides", () => { + const result = normalizeWorkflowConfig(VALID_CONFIG); + expect(result.modelOverrides).toBeNull(); + }); + + it("returns null for null agentOverrides", () => { + const result = normalizeWorkflowConfig({ ...VALID_CONFIG, agentOverrides: null }); + expect(result.agentOverrides).toBeNull(); + }); + + it("normalizes agentOverrides with nested roles", () => { + const config = { + ...VALID_CONFIG, + agentOverrides: { + "solve-issue": { coder: "hermes", reviewer: "claude" }, + }, + }; + const result = normalizeWorkflowConfig(config); + expect(result.agentOverrides).toEqual({ + "solve-issue": { coder: "hermes", reviewer: "claude" }, + }); + }); + + it("normalizes modelOverrides", () => { + const config = { ...VALID_CONFIG, modelOverrides: { coding: "fast" } }; + const result = normalizeWorkflowConfig(config); + expect(result.modelOverrides).toEqual({ coding: "fast" }); + }); +}); diff --git a/packages/util/__tests__/base32.test.ts b/packages/util/__tests__/base32.test.ts new file mode 100644 index 0000000..2d2ba53 --- /dev/null +++ b/packages/util/__tests__/base32.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, it } from "vitest"; +import { + CROCKFORD_BASE32_ALPHABET, + encodeCrockfordBase32Bits, + decodeCrockfordBase32Bits, + encodeUint64AsCrockford, + decodeCrockfordToUint64, +} from "../src/base32.js"; + +describe("CROCKFORD_BASE32_ALPHABET", () => { + it("has exactly 32 characters", () => { + expect(CROCKFORD_BASE32_ALPHABET).toHaveLength(32); + }); + + it("excludes I, L, O, U", () => { + expect(CROCKFORD_BASE32_ALPHABET).not.toContain("I"); + expect(CROCKFORD_BASE32_ALPHABET).not.toContain("L"); + expect(CROCKFORD_BASE32_ALPHABET).not.toContain("O"); + expect(CROCKFORD_BASE32_ALPHABET).not.toContain("U"); + }); +}); + +describe("encodeCrockfordBase32Bits / decodeCrockfordBase32Bits", () => { + it("roundtrips zero with bitLength=5", () => { + const encoded = encodeCrockfordBase32Bits(0n, 5); + expect(encoded).toBe("0"); + const decoded = decodeCrockfordBase32Bits(encoded, 5); + expect(decoded).toEqual({ ok: true, value: 0n }); + }); + + it("roundtrips value 31 with bitLength=5", () => { + const encoded = encodeCrockfordBase32Bits(31n, 5); + expect(encoded).toBe("Z"); + const decoded = decodeCrockfordBase32Bits(encoded, 5); + expect(decoded).toEqual({ ok: true, value: 31n }); + }); + + it("roundtrips with bitLength=10", () => { + const encoded = encodeCrockfordBase32Bits(1023n, 10); + expect(encoded).toBe("ZZ"); + const decoded = decodeCrockfordBase32Bits(encoded, 10); + expect(decoded).toEqual({ ok: true, value: 1023n }); + }); + + it("roundtrips with non-multiple-of-5 bitLength", () => { + const value = 255n; // 8 bits + const encoded = encodeCrockfordBase32Bits(value, 8); + expect(encoded).toHaveLength(2); // 8 bits -> 10 bits padded -> 2 chars + const decoded = decodeCrockfordBase32Bits(encoded, 8); + expect(decoded).toEqual({ ok: true, value }); + }); + + it("roundtrips large value", () => { + const value = (1n << 64n) - 1n; + const encoded = encodeCrockfordBase32Bits(value, 64); + const decoded = decodeCrockfordBase32Bits(encoded, 64); + expect(decoded).toEqual({ ok: true, value }); + }); + + it("throws on bitLength <= 0", () => { + expect(() => encodeCrockfordBase32Bits(0n, 0)).toThrow("bitLength must be positive"); + expect(() => encodeCrockfordBase32Bits(0n, -1)).toThrow("bitLength must be positive"); + }); + + it("returns error on decode with bitLength <= 0", () => { + const result = decodeCrockfordBase32Bits("0", 0); + expect(result.ok).toBe(false); + }); + + it("returns error on invalid character", () => { + const result = decodeCrockfordBase32Bits("U", 5); + expect(result.ok).toBe(false); + }); + + it("returns error on wrong encoded length", () => { + const result = decodeCrockfordBase32Bits("00", 5); + expect(result.ok).toBe(false); + }); + + it("handles lowercase input on decode", () => { + const encoded = encodeCrockfordBase32Bits(10n, 5); + const decoded = decodeCrockfordBase32Bits(encoded.toLowerCase(), 5); + expect(decoded).toEqual({ ok: true, value: 10n }); + }); +}); + +describe("encodeUint64AsCrockford / decodeCrockfordToUint64", () => { + it("encodes to 13 characters", () => { + expect(encodeUint64AsCrockford(0n)).toHaveLength(13); + expect(encodeUint64AsCrockford(1n)).toHaveLength(13); + }); + + it("roundtrips 0n", () => { + const encoded = encodeUint64AsCrockford(0n); + expect(encoded).toBe("0000000000000"); + const decoded = decodeCrockfordToUint64(encoded); + expect(decoded).toEqual({ ok: true, value: 0n }); + }); + + it("roundtrips max uint64", () => { + const max = (1n << 64n) - 1n; + const encoded = encodeUint64AsCrockford(max); + const decoded = decodeCrockfordToUint64(encoded); + expect(decoded).toEqual({ ok: true, value: max }); + }); + + it("roundtrips arbitrary value", () => { + const value = 0xDEAD_BEEF_CAFE_BABEn; + const encoded = encodeUint64AsCrockford(value); + const decoded = decodeCrockfordToUint64(encoded); + expect(decoded).toEqual({ ok: true, value }); + }); + + it("masks values beyond 64 bits", () => { + const over = (1n << 64n) + 42n; + const encoded = encodeUint64AsCrockford(over); + const decoded = decodeCrockfordToUint64(encoded); + expect(decoded).toEqual({ ok: true, value: 42n }); + }); + + it("returns error for invalid input", () => { + const result = decodeCrockfordToUint64("!!!"); + expect(result.ok).toBe(false); + }); + + it("returns error for wrong length", () => { + const result = decodeCrockfordToUint64("000"); + expect(result.ok).toBe(false); + }); +}); diff --git a/packages/util/__tests__/log-tag.test.ts b/packages/util/__tests__/log-tag.test.ts new file mode 100644 index 0000000..7d8cb4b --- /dev/null +++ b/packages/util/__tests__/log-tag.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest'; +import { assertValidLogTag } from '../src/process-logger/log-tag.js'; + +describe('assertValidLogTag', () => { + it('accepts valid 8-char Crockford Base32 tags', () => { + expect(() => assertValidLogTag('0123ABCD')).not.toThrow(); + expect(() => assertValidLogTag('VWXYZ789')).not.toThrow(); + expect(() => assertValidLogTag('00000000')).not.toThrow(); + expect(() => assertValidLogTag('ZZZZZZZZ')).not.toThrow(); + }); + + it('accepts lowercase (converted via toUpperCase)', () => { + expect(() => assertValidLogTag('abcdefgh')).not.toThrow(); + expect(() => assertValidLogTag('0a1b2c3d')).not.toThrow(); + }); + + it('throws on too short', () => { + expect(() => assertValidLogTag('1234567')).toThrow(); + expect(() => assertValidLogTag('')).toThrow(); + }); + + it('throws on too long', () => { + expect(() => assertValidLogTag('123456789')).toThrow(); + }); + + it('throws on invalid chars I, L, O, U', () => { + expect(() => assertValidLogTag('IIIIIIII')).toThrow(); + expect(() => assertValidLogTag('LLLLLLLL')).toThrow(); + expect(() => assertValidLogTag('OOOOOOOO')).toThrow(); + expect(() => assertValidLogTag('UUUUUUUU')).toThrow(); + }); + + it('throws on special characters', () => { + expect(() => assertValidLogTag('1234567!')).toThrow(); + expect(() => assertValidLogTag('ABCD-EFG')).toThrow(); + expect(() => assertValidLogTag('ABCD EFG')).toThrow(); + }); +}); diff --git a/packages/util/__tests__/refs-field.test.ts b/packages/util/__tests__/refs-field.test.ts new file mode 100644 index 0000000..6e3ec8e --- /dev/null +++ b/packages/util/__tests__/refs-field.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest'; +import { mergeRefsWithContentHash, normalizeRefsField } from '../src/refs-field.js'; + +describe('mergeRefsWithContentHash', () => { + it('appends a new content hash', () => { + expect(mergeRefsWithContentHash(['a', 'b'], 'c')).toEqual(['a', 'b', 'c']); + }); + + it('skips duplicate content hash', () => { + expect(mergeRefsWithContentHash(['a', 'b'], 'b')).toEqual(['a', 'b']); + }); + + it('preserves order', () => { + expect(mergeRefsWithContentHash(['x', 'y'], 'z')).toEqual(['x', 'y', 'z']); + }); + + it('handles empty refs', () => { + expect(mergeRefsWithContentHash([], 'a')).toEqual(['a']); + }); +}); + +describe('normalizeRefsField', () => { + it('returns empty array for non-array', () => { + expect(normalizeRefsField(null)).toEqual([]); + expect(normalizeRefsField(undefined)).toEqual([]); + expect(normalizeRefsField(42)).toEqual([]); + }); + + it('passes through string array', () => { + expect(normalizeRefsField(['a', 'b'])).toEqual(['a', 'b']); + }); + + it('filters non-strings from mixed array', () => { + expect(normalizeRefsField(['a', 1, 'b', null])).toEqual(['a', 'b']); + }); + + it('handles empty array', () => { + expect(normalizeRefsField([])).toEqual([]); + }); +}); diff --git a/packages/util/__tests__/result.test.ts b/packages/util/__tests__/result.test.ts new file mode 100644 index 0000000..7ec6977 --- /dev/null +++ b/packages/util/__tests__/result.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; +import { ok, err } from '../src/result.js'; + +describe('result', () => { + describe('ok', () => { + it('wraps a value', () => { + const r = ok(42); + expect(r).toEqual({ ok: true, value: 42 }); + }); + + it('wraps a string value', () => { + const r = ok('hello'); + expect(r.ok).toBe(true); + if (r.ok) expect(r.value).toBe('hello'); + }); + }); + + describe('err', () => { + it('wraps an error', () => { + const r = err('fail'); + expect(r).toEqual({ ok: false, error: 'fail' }); + }); + + it('wraps an Error object', () => { + const e = new Error('boom'); + const r = err(e); + expect(r.ok).toBe(false); + if (!r.ok) expect(r.error).toBe(e); + }); + }); + + describe('type narrowing', () => { + it('narrows ok result', () => { + const r = ok(10) as ReturnType> | ReturnType>; + if (r.ok) { + expect(r.value).toBe(10); + } else { + expect.unreachable(); + } + }); + + it('narrows err result', () => { + const r = err('bad') as ReturnType> | ReturnType>; + if (!r.ok) { + expect(r.error).toBe('bad'); + } else { + expect.unreachable(); + } + }); + }); +}); diff --git a/packages/util/__tests__/storage-root.test.ts b/packages/util/__tests__/storage-root.test.ts new file mode 100644 index 0000000..1e60f60 --- /dev/null +++ b/packages/util/__tests__/storage-root.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; +import { homedir } from 'node:os'; +import { getDefaultStorageRoot, getDefaultWorkflowStorageRoot, getGlobalCasDir } from '../src/storage-root.js'; + +describe('getDefaultStorageRoot', () => { + it('returns homedir + /.uwf', () => { + expect(getDefaultStorageRoot()).toBe(homedir() + '/.uwf'); + }); +}); + +describe('getDefaultWorkflowStorageRoot', () => { + it('returns same as getDefaultStorageRoot (deprecated alias)', () => { + expect(getDefaultWorkflowStorageRoot()).toBe(getDefaultStorageRoot()); + }); +}); + +describe('getGlobalCasDir', () => { + it('appends /cas to given storage root', () => { + expect(getGlobalCasDir('/tmp/test')).toBe('/tmp/test/cas'); + }); + + it('falls back to default when undefined', () => { + expect(getGlobalCasDir(undefined)).toBe(homedir() + '/.uwf/cas'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4ca89e..0bbdfce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@united-workforce/agent-hermes': specifier: workspace:* version: link:packages/agent-hermes + '@vitest/coverage-v8': + specifier: ^4.1.8 + version: 4.1.8(vitest@3.2.6(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(msw@2.14.6(@types/node@25.9.1)(typescript@5.9.3))(yaml@2.9.0)) typescript: specifier: ^5.8.3 version: 5.9.3 @@ -411,6 +414,10 @@ packages: '@types/react': optional: true + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@biomejs/biome@2.4.16': resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==} engines: {node: '>=14.21.3'} @@ -1293,6 +1300,15 @@ packages: babel-plugin-react-compiler: optional: true + '@vitest/coverage-v8@4.1.8': + resolution: {integrity: sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==} + peerDependencies: + '@vitest/browser': 4.1.8 + vitest: 4.1.8 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@3.2.6': resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==} @@ -1310,6 +1326,9 @@ packages: '@vitest/pretty-format@3.2.6': resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==} + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} + '@vitest/runner@3.2.6': resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==} @@ -1322,6 +1341,9 @@ packages: '@vitest/utils@3.2.6': resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==} + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + '@xyflow/react@12.11.0': resolution: {integrity: sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA==} peerDependencies: @@ -1391,6 +1413,9 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} + ast-v8-to-istanbul@1.0.3: + resolution: {integrity: sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -1952,6 +1977,10 @@ packages: resolution: {integrity: sha512-cQOsSMS/IrDz82PVyRDvf/Q1F/bRbBVjJlh+xYOkI1qw2bWRvWGiWc+m2O0d6l4Bt1fyY+8kzJ8JFWGJqNeDBg==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1967,6 +1996,9 @@ packages: resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} engines: {node: '>=16.9.0'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -2106,6 +2138,18 @@ packages: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -2113,6 +2157,9 @@ packages: jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2267,6 +2314,13 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2383,6 +2437,9 @@ packages: resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} engines: {node: '>= 10'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2731,6 +2788,9 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -2777,6 +2837,10 @@ packages: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -2816,6 +2880,10 @@ packages: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + tinyspy@4.0.4: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} @@ -3334,6 +3402,8 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 + '@bcoe/v8-coverage@1.0.2': {} + '@biomejs/biome@2.4.16': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.4.16 @@ -4093,6 +4163,20 @@ snapshots: '@rolldown/pluginutils': 1.0.1 vite: 8.0.16(@types/node@25.9.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0) + '@vitest/coverage-v8@4.1.8(vitest@3.2.6(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(msw@2.14.6(@types/node@25.9.1)(typescript@5.9.3))(yaml@2.9.0))': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.8 + ast-v8-to-istanbul: 1.0.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.1 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 3.2.6(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(msw@2.14.6(@types/node@25.9.1)(typescript@5.9.3))(yaml@2.9.0) + '@vitest/expect@3.2.6': dependencies: '@types/chai': 5.2.3 @@ -4114,6 +4198,10 @@ snapshots: dependencies: tinyrainbow: 2.0.0 + '@vitest/pretty-format@4.1.8': + dependencies: + tinyrainbow: 3.1.0 + '@vitest/runner@3.2.6': dependencies: '@vitest/utils': 3.2.6 @@ -4136,6 +4224,12 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + '@vitest/utils@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@xyflow/react@12.11.0(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(immer@11.1.8)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@xyflow/system': 0.0.77 @@ -4203,6 +4297,12 @@ snapshots: dependencies: tslib: 2.8.1 + ast-v8-to-istanbul@1.0.3: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + balanced-match@4.0.4: {} baseline-browser-mapping@2.10.33: {} @@ -4760,6 +4860,8 @@ snapshots: graphql@16.14.1: {} + has-flag@4.0.0: {} + has-symbols@1.1.0: {} hasown@2.0.4: @@ -4773,6 +4875,8 @@ snapshots: hono@4.12.23: {} + html-escaper@2.0.2: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -4869,10 +4973,25 @@ snapshots: isexe@3.1.5: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jiti@2.7.0: {} jose@6.2.3: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -4990,6 +5109,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.1 + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -5085,6 +5214,8 @@ snapshots: object-treeify@1.1.33: {} + obug@2.1.1: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -5497,6 +5628,8 @@ snapshots: std-env@3.10.0: {} + std-env@4.1.0: {} + stdin-discarder@0.2.2: {} strict-event-emitter@0.5.1: {} @@ -5541,6 +5674,10 @@ snapshots: dependencies: '@tokenizer/token': 0.3.0 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + tagged-tag@1.0.0: {} tailwind-merge@3.6.0: {} @@ -5566,6 +5703,8 @@ snapshots: tinyrainbow@2.0.0: {} + tinyrainbow@3.1.0: {} + tinyspy@4.0.4: {} tldts-core@7.4.2: {}