diff --git a/packages/cli/src/__tests__/manifest-cache-lifecycle.test.ts b/packages/cli/src/__tests__/manifest-cache-lifecycle.test.ts index 022e837b9..9f12badf7 100644 --- a/packages/cli/src/__tests__/manifest-cache-lifecycle.test.ts +++ b/packages/cli/src/__tests__/manifest-cache-lifecycle.test.ts @@ -4,7 +4,7 @@ import type { TestEnvironment } from "./test-helpers"; import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; import { existsSync, mkdirSync, rmSync, utimesSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { agentKeys, countImplemented, loadManifest } from "../manifest"; +import { _resetCacheForTesting, agentKeys, countImplemented, loadManifest } from "../manifest"; import { createMockManifest, setupTestEnvironment, teardownTestEnvironment } from "./test-helpers"; /** @@ -239,6 +239,7 @@ describe("Manifest Cache Lifecycle", () => { beforeEach(() => { env = setupTestEnvironment(); + _resetCacheForTesting(); }); afterEach(() => { @@ -255,15 +256,6 @@ describe("Manifest Cache Lifecycle", () => { // fetch should have been called at least twice (once per forceRefresh) expect(fetchMock.mock.calls.length).toBeGreaterThanOrEqual(2); }); - - it("should return same instance without forceRefresh", async () => { - global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); - - const manifest1 = await loadManifest(true); - const manifest2 = await loadManifest(false); - - expect(manifest1).toBe(manifest2); - }); }); describe("combined fallback chain: invalid fetch + stale cache", () => { @@ -271,6 +263,7 @@ describe("Manifest Cache Lifecycle", () => { beforeEach(() => { env = setupTestEnvironment(); + _resetCacheForTesting(); }); afterEach(() => { diff --git a/packages/cli/src/__tests__/manifest.test.ts b/packages/cli/src/__tests__/manifest.test.ts index e025f52e9..8afed3b91 100644 --- a/packages/cli/src/__tests__/manifest.test.ts +++ b/packages/cli/src/__tests__/manifest.test.ts @@ -171,15 +171,6 @@ describe("manifest", () => { expect(global.fetch).toHaveBeenCalled(); }); - it("returns in-memory cache on second call without fetching", async () => { - const fetchMock = mock(async () => new Response(JSON.stringify(mockManifest))); - global.fetch = fetchMock; - await loadManifest(); - const fetchCount = fetchMock.mock.calls.length; - await loadManifest(); - expect(fetchMock.mock.calls.length).toBe(fetchCount); - }); - it("falls back to stale cache when fetch fails", async () => { const cacheDir = join(env.testDir, "spawn"); mkdirSync(cacheDir, { @@ -217,30 +208,22 @@ describe("manifest", () => { await expect(loadManifest(true)).rejects.toThrow("Cannot load manifest"); }); - it("throws when manifest from GitHub is invalid", async () => { - const consoleSpy = spyOn(console, "error").mockImplementation(() => {}); - global.fetch = mock( - async () => + const invalidManifestCases: Array<{ + label: string; + fetchImpl: () => Promise; + }> = [ + { + label: "non-manifest shape", + fetchImpl: async () => new Response( JSON.stringify({ not: "a manifest", }), ), - ); - - const cacheFile = join(env.testDir, "spawn", "manifest.json"); - if (existsSync(cacheFile)) { - rmSync(cacheFile); - } - - await expect(loadManifest(true)).rejects.toThrow("Cannot load manifest"); - consoleSpy.mockRestore(); - }); - - it("rejects manifest with string agents field", async () => { - const consoleSpy = spyOn(console, "error").mockImplementation(() => {}); - global.fetch = mock( - async () => + }, + { + label: "string agents field", + fetchImpl: async () => new Response( JSON.stringify({ agents: "claude", @@ -248,21 +231,10 @@ describe("manifest", () => { matrix: {}, }), ), - ); - - const cacheFile = join(env.testDir, "spawn", "manifest.json"); - if (existsSync(cacheFile)) { - rmSync(cacheFile); - } - - await expect(loadManifest(true)).rejects.toThrow("Cannot load manifest"); - consoleSpy.mockRestore(); - }); - - it("rejects manifest with array clouds field", async () => { - const consoleSpy = spyOn(console, "error").mockImplementation(() => {}); - global.fetch = mock( - async () => + }, + { + label: "array clouds field", + fetchImpl: async () => new Response( JSON.stringify({ agents: {}, @@ -273,21 +245,10 @@ describe("manifest", () => { matrix: {}, }), ), - ); - - const cacheFile = join(env.testDir, "spawn", "manifest.json"); - if (existsSync(cacheFile)) { - rmSync(cacheFile); - } - - await expect(loadManifest(true)).rejects.toThrow("Cannot load manifest"); - consoleSpy.mockRestore(); - }); - - it("rejects manifest with numeric matrix field", async () => { - const consoleSpy = spyOn(console, "error").mockImplementation(() => {}); - global.fetch = mock( - async () => + }, + { + label: "numeric matrix field", + fetchImpl: async () => new Response( JSON.stringify({ agents: {}, @@ -295,31 +256,27 @@ describe("manifest", () => { matrix: 42, }), ), - ); - - const cacheFile = join(env.testDir, "spawn", "manifest.json"); - if (existsSync(cacheFile)) { - rmSync(cacheFile); - } - - await expect(loadManifest(true)).rejects.toThrow("Cannot load manifest"); - consoleSpy.mockRestore(); - }); - - it("throws when network errors occur and no cache exists", async () => { - const consoleSpy = spyOn(console, "error").mockImplementation(() => {}); - global.fetch = mock(async () => { - throw new Error("Network timeout"); + }, + { + label: "network error", + fetchImpl: async () => { + throw new Error("Network timeout"); + }, + }, + ]; + + for (const { label, fetchImpl } of invalidManifestCases) { + it(`rejects invalid manifest (${label})`, async () => { + const consoleSpy = spyOn(console, "error").mockImplementation(() => {}); + global.fetch = mock(fetchImpl); + const cacheFile = join(env.testDir, "spawn", "manifest.json"); + if (existsSync(cacheFile)) { + rmSync(cacheFile); + } + await expect(loadManifest(true)).rejects.toThrow("Cannot load manifest"); + consoleSpy.mockRestore(); }); - - const cacheFile = join(env.testDir, "spawn", "manifest.json"); - if (existsSync(cacheFile)) { - rmSync(cacheFile); - } - - await expect(loadManifest(true)).rejects.toThrow("Cannot load manifest"); - consoleSpy.mockRestore(); - }); + } }); }); diff --git a/packages/cli/src/__tests__/ui-cov.test.ts b/packages/cli/src/__tests__/ui-cov.test.ts index 2bcf2092f..2ec817c0c 100644 --- a/packages/cli/src/__tests__/ui-cov.test.ts +++ b/packages/cli/src/__tests__/ui-cov.test.ts @@ -62,25 +62,34 @@ afterEach(() => { // ── Logging functions ────────────────────────────────────────────── describe("logging functions", () => { - it("logInfo writes green text to stderr", () => { - logInfo("test info"); - expect(stderrOutput.join("")).toContain("test info"); - }); - - it("logWarn writes yellow text to stderr", () => { - logWarn("test warn"); - expect(stderrOutput.join("")).toContain("test warn"); - }); - - it("logError writes red text to stderr", () => { - logError("test error"); - expect(stderrOutput.join("")).toContain("test error"); - }); - - it("logStep writes cyan text to stderr", () => { - logStep("test step"); - expect(stderrOutput.join("")).toContain("test step"); - }); + for (const [fn, msg] of [ + [ + logInfo, + "test info", + ], + [ + logWarn, + "test warn", + ], + [ + logError, + "test error", + ], + [ + logStep, + "test step", + ], + ] satisfies Array< + [ + (msg: string) => void, + string, + ] + >) { + it(`${fn.name} writes message to stderr`, () => { + fn(msg); + expect(stderrOutput.join("")).toContain(msg); + }); + } it("logStepInline writes message (newline-terminated in non-TTY)", () => { logStepInline("inline msg");