From da86b57f2311cd84995f2536430b056a2f920506 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 5 Apr 2026 01:01:19 +0900 Subject: [PATCH] test: fix CI test isolation - upgrade Bun, fix mock contamination and fresh-import patterns --- .github/workflows/ci.yml | 8 +-- bun.lock | 4 +- package.json | 2 +- .../execute-http-hook-security.test.ts | 61 ++++++++++++------- src/mcp/zauc-mocks-mcp-index/index.test.ts | 18 ++++-- .../subagent-resolver.test.ts | 12 ++-- .../zauc-mocks-skill-tools/tools.test.ts | 15 +++-- 7 files changed, 69 insertions(+), 51 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 179fe876b..af24ea533 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,11 +37,9 @@ jobs: steps: - uses: actions/checkout@v4 - # Pin to 1.3.10: bun 1.3.11 breaks spyOn on ESM barrel re-exports, - # causing 7 createBuiltinAgents tests to fail in batch execution. - uses: oven-sh/setup-bun@v2 with: - bun-version: "1.3.10" + bun-version: "1.3.11" - name: Install dependencies run: bun install @@ -58,7 +56,7 @@ jobs: - uses: oven-sh/setup-bun@v2 with: - bun-version: "1.3.10" + bun-version: "1.3.11" - name: Install dependencies run: bun install @@ -83,7 +81,7 @@ jobs: - uses: oven-sh/setup-bun@v2 with: - bun-version: "1.3.10" + bun-version: "1.3.11" - name: Install dependencies run: bun install diff --git a/bun.lock b/bun.lock index 4e96d0f2c..24b3733e1 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,7 @@ "devDependencies": { "@types/js-yaml": "^4.0.9", "@types/picomatch": "^3.0.2", - "bun-types": "1.3.10", + "bun-types": "1.3.11", "typescript": "^5.7.3", }, "optionalDependencies": { @@ -118,7 +118,7 @@ "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], diff --git a/package.json b/package.json index d88d831eb..85e631c9c 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "devDependencies": { "@types/js-yaml": "^4.0.9", "@types/picomatch": "^3.0.2", - "bun-types": "1.3.10", + "bun-types": "1.3.11", "typescript": "^5.7.3" }, "optionalDependencies": { diff --git a/src/hooks/claude-code-hooks/execute-http-hook-security.test.ts b/src/hooks/claude-code-hooks/execute-http-hook-security.test.ts index 16dbb1de1..11e08a668 100644 --- a/src/hooks/claude-code-hooks/execute-http-hook-security.test.ts +++ b/src/hooks/claude-code-hooks/execute-http-hook-security.test.ts @@ -1,13 +1,12 @@ /// -import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test" +import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from "bun:test" import type { HookHttp } from "./types" +import * as sharedLogger from "../../shared/logger" const mockFetch = mock(() => Promise.resolve(new Response(JSON.stringify({}), { status: 200 })) ) -const mockLog = mock(() => {}) - const originalFetch = globalThis.fetch const originalEnv = process.env @@ -17,6 +16,8 @@ async function importFreshExecuteHttpHook() { } describe("executeHttpHook TLS security", () => { + let logSpy: ReturnType | undefined + beforeEach(() => { globalThis.fetch = mockFetch as unknown as typeof fetch mockFetch.mockReset() @@ -28,7 +29,8 @@ describe("executeHttpHook TLS security", () => { afterEach(() => { globalThis.fetch = originalFetch process.env = { ...originalEnv } - mockLog.mockReset() + logSpy?.mockRestore() + logSpy = undefined mockFetch.mockReset() mock.restore() }) @@ -61,18 +63,22 @@ describe("executeHttpHook TLS security", () => { }) it("#when hook uses remote http:// URL #then logs warning before rejection", async () => { - mock.module("../../shared/logger", () => ({ - log: mockLog, - })) + // given + logSpy = spyOn(sharedLogger, "log").mockImplementation(() => {}) const { executeHttpHook } = await importFreshExecuteHttpHook() - const hook: HookHttp = { type: "http", url: "http://example.com/hooks" } + const hook: HookHttp = { type: "http", url: "http://tls-security-remote.invalid/hooks" } + // when const result = await executeHttpHook(hook, "{}") - expect(result.exitCode).toBe(1) - expect(mockLog).toHaveBeenCalledWith("HTTP hook URL uses insecure protocol", { - url: "http://example.com/hooks", + // then + const matchingCalls = logSpy.mock.calls.filter(([message, data]) => { + return message === "HTTP hook URL uses insecure protocol" + && JSON.stringify(data) === JSON.stringify({ url: hook.url }) }) + + expect(result.exitCode).toBe(1) + expect(matchingCalls).toHaveLength(1) expect(mockFetch).not.toHaveBeenCalled() }) @@ -87,17 +93,22 @@ describe("executeHttpHook TLS security", () => { }) it("#when hook uses http://localhost #then does not log insecure warning", async () => { - mock.module("../../shared/logger", () => ({ - log: mockLog, - })) - mockLog.mockReset() + // given + logSpy = spyOn(sharedLogger, "log").mockImplementation(() => {}) const { executeHttpHook } = await importFreshExecuteHttpHook() - const hook: HookHttp = { type: "http", url: "http://localhost:8080/hooks" } + const hook: HookHttp = { type: "http", url: "http://localhost:49123/hooks" } + // when const result = await executeHttpHook(hook, "{}") + // then + const matchingCalls = logSpy.mock.calls.filter(([message, data]) => { + return message === "HTTP hook URL uses insecure protocol" + && JSON.stringify(data) === JSON.stringify({ url: hook.url }) + }) + expect(result.exitCode).toBe(0) - expect(mockLog).not.toHaveBeenCalled() + expect(matchingCalls).toHaveLength(0) }) it("#when hook uses http://127.0.0.1 #then allows execution", async () => { @@ -158,17 +169,21 @@ describe("executeHttpHook TLS security", () => { }) it("#when hook uses plain remote http:// URL #then writes warning log", async () => { - mock.module("../../shared/logger", () => ({ - log: mockLog, - })) + // given + logSpy = spyOn(sharedLogger, "log").mockImplementation(() => {}) const { executeHttpHook } = await importFreshExecuteHttpHook() - const hook: HookHttp = { type: "http", url: "http://example.com/hooks" } + const hook: HookHttp = { type: "http", url: "http://tls-security-dev.invalid/hooks" } + // when await executeHttpHook(hook, "{}") - expect(mockLog).toHaveBeenCalledWith("HTTP hook URL uses insecure protocol", { - url: "http://example.com/hooks", + // then + const matchingCalls = logSpy.mock.calls.filter(([message, data]) => { + return message === "HTTP hook URL uses insecure protocol" + && JSON.stringify(data) === JSON.stringify({ url: hook.url }) }) + + expect(matchingCalls).toHaveLength(1) }) it("#when hook uses http://[::1] #then allows execution", async () => { diff --git a/src/mcp/zauc-mocks-mcp-index/index.test.ts b/src/mcp/zauc-mocks-mcp-index/index.test.ts index ce772ac99..f14215b56 100644 --- a/src/mcp/zauc-mocks-mcp-index/index.test.ts +++ b/src/mcp/zauc-mocks-mcp-index/index.test.ts @@ -10,7 +10,10 @@ describe("createBuiltinMcps", () => { const result = createBuiltinMcps(disabledMcps) // then - expect(result.length).toBeGreaterThan(0) + expect(Object.keys(result).length).toBeGreaterThan(0) + expect(result.websearch).toBeDefined() + expect(result.context7).toBeDefined() + expect(result.grep_app).toBeDefined() }) test("should filter out disabled MCPs", () => { @@ -21,20 +24,23 @@ describe("createBuiltinMcps", () => { const result = createBuiltinMcps(disabledMcps) // then - expect(result.some((mcp) => mcp.name === "websearch")).toBe(false) + expect(result.websearch).toBeUndefined() + expect(result.context7).toBeDefined() + expect(result.grep_app).toBeDefined() }) test("should return empty array when all MCPs are disabled", () => { // given - disable all known MCPs - const disabledMcps = ["websearch", "context7", "grep-app"] + const disabledMcps = ["websearch", "context7", "grep_app"] // when const result = createBuiltinMcps(disabledMcps) // then - may still have MCPs we didn't list - const remainingMcpNames = result.map((m) => m.name) + const remainingMcpNames = Object.keys(result) expect(remainingMcpNames).not.toContain("websearch") expect(remainingMcpNames).not.toContain("context7") - expect(remainingMcpNames).not.toContain("grep-app") + expect(remainingMcpNames).not.toContain("grep_app") + expect(remainingMcpNames).toEqual([]) }) -}) \ No newline at end of file +}) diff --git a/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts index 777731c40..e21b104fb 100644 --- a/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts +++ b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts @@ -1,14 +1,14 @@ declare const require: (name: string) => any const { describe, test, expect, beforeEach, afterEach, spyOn, mock } = require("bun:test") -import type { DelegateTaskArgs } from "../../types" -import type { ExecutorContext } from "../../executor-types" -import * as logger from "../../../../shared/logger" -import * as connectedProvidersCache from "../../../../shared/connected-providers-cache" +import type { DelegateTaskArgs } from "../types" +import type { ExecutorContext } from "../executor-types" +import * as logger from "../../../shared/logger" +import * as connectedProvidersCache from "../../../shared/connected-providers-cache" -type SubagentResolverModule = typeof import("../../subagent-resolver") +type SubagentResolverModule = typeof import("../subagent-resolver") async function importFreshSubagentResolverModule(): Promise { - return await import(`../../subagent-resolver?test=${Date.now()}-${Math.random()}`) + return await import(`../subagent-resolver?test=${Date.now()}-${Math.random()}`) } function createBaseArgs(overrides?: Partial): DelegateTaskArgs { diff --git a/src/tools/skill/zauc-mocks-skill-tools/tools.test.ts b/src/tools/skill/zauc-mocks-skill-tools/tools.test.ts index 67bc6876e..785428c1c 100644 --- a/src/tools/skill/zauc-mocks-skill-tools/tools.test.ts +++ b/src/tools/skill/zauc-mocks-skill-tools/tools.test.ts @@ -8,7 +8,9 @@ import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js" const originalReadFileSync = fs.readFileSync.bind(fs) -async function importFreshSkillToolModule(): Promise { +let createSkillTool: typeof import("../tools").createSkillTool + +beforeEach(async () => { mock.module("node:fs", () => ({ ...fs, readFileSync: (path: string, encoding?: string) => { @@ -21,13 +23,10 @@ Test skill body content` return originalReadFileSync(path, encoding as BufferEncoding) }, })) - - const module = await import(`./tools?test=${Date.now()}-${Math.random()}`) - mock.restore() - return module -} - -const { createSkillTool } = await importFreshSkillToolModule() + + const module = await import("../tools") + createSkillTool = module.createSkillTool +}) afterAll(() => { mock.restore()