test: fix CI test isolation - upgrade Bun, fix mock contamination and fresh-import patterns

This commit is contained in:
YeonGyu-Kim
2026-04-05 01:01:19 +09:00
parent aa7e2aa07f
commit da86b57f23
7 changed files with 69 additions and 51 deletions
+3 -5
View File
@@ -37,11 +37,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - 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 - uses: oven-sh/setup-bun@v2
with: with:
bun-version: "1.3.10" bun-version: "1.3.11"
- name: Install dependencies - name: Install dependencies
run: bun install run: bun install
@@ -58,7 +56,7 @@ jobs:
- uses: oven-sh/setup-bun@v2 - uses: oven-sh/setup-bun@v2
with: with:
bun-version: "1.3.10" bun-version: "1.3.11"
- name: Install dependencies - name: Install dependencies
run: bun install run: bun install
@@ -83,7 +81,7 @@ jobs:
- uses: oven-sh/setup-bun@v2 - uses: oven-sh/setup-bun@v2
with: with:
bun-version: "1.3.10" bun-version: "1.3.11"
- name: Install dependencies - name: Install dependencies
run: bun install run: bun install
+2 -2
View File
@@ -25,7 +25,7 @@
"devDependencies": { "devDependencies": {
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/picomatch": "^3.0.2", "@types/picomatch": "^3.0.2",
"bun-types": "1.3.10", "bun-types": "1.3.11",
"typescript": "^5.7.3", "typescript": "^5.7.3",
}, },
"optionalDependencies": { "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=="], "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=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
+1 -1
View File
@@ -74,7 +74,7 @@
"devDependencies": { "devDependencies": {
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/picomatch": "^3.0.2", "@types/picomatch": "^3.0.2",
"bun-types": "1.3.10", "bun-types": "1.3.11",
"typescript": "^5.7.3" "typescript": "^5.7.3"
}, },
"optionalDependencies": { "optionalDependencies": {
@@ -1,13 +1,12 @@
/// <reference types="bun-types" /> /// <reference types="bun-types" />
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 type { HookHttp } from "./types"
import * as sharedLogger from "../../shared/logger"
const mockFetch = mock(() => const mockFetch = mock(() =>
Promise.resolve(new Response(JSON.stringify({}), { status: 200 })) Promise.resolve(new Response(JSON.stringify({}), { status: 200 }))
) )
const mockLog = mock(() => {})
const originalFetch = globalThis.fetch const originalFetch = globalThis.fetch
const originalEnv = process.env const originalEnv = process.env
@@ -17,6 +16,8 @@ async function importFreshExecuteHttpHook() {
} }
describe("executeHttpHook TLS security", () => { describe("executeHttpHook TLS security", () => {
let logSpy: ReturnType<typeof spyOn> | undefined
beforeEach(() => { beforeEach(() => {
globalThis.fetch = mockFetch as unknown as typeof fetch globalThis.fetch = mockFetch as unknown as typeof fetch
mockFetch.mockReset() mockFetch.mockReset()
@@ -28,7 +29,8 @@ describe("executeHttpHook TLS security", () => {
afterEach(() => { afterEach(() => {
globalThis.fetch = originalFetch globalThis.fetch = originalFetch
process.env = { ...originalEnv } process.env = { ...originalEnv }
mockLog.mockReset() logSpy?.mockRestore()
logSpy = undefined
mockFetch.mockReset() mockFetch.mockReset()
mock.restore() mock.restore()
}) })
@@ -61,18 +63,22 @@ describe("executeHttpHook TLS security", () => {
}) })
it("#when hook uses remote http:// URL #then logs warning before rejection", async () => { it("#when hook uses remote http:// URL #then logs warning before rejection", async () => {
mock.module("../../shared/logger", () => ({ // given
log: mockLog, logSpy = spyOn(sharedLogger, "log").mockImplementation(() => {})
}))
const { executeHttpHook } = await importFreshExecuteHttpHook() 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, "{}") const result = await executeHttpHook(hook, "{}")
expect(result.exitCode).toBe(1) // then
expect(mockLog).toHaveBeenCalledWith("HTTP hook URL uses insecure protocol", { const matchingCalls = logSpy.mock.calls.filter(([message, data]) => {
url: "http://example.com/hooks", 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() 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 () => { it("#when hook uses http://localhost #then does not log insecure warning", async () => {
mock.module("../../shared/logger", () => ({ // given
log: mockLog, logSpy = spyOn(sharedLogger, "log").mockImplementation(() => {})
}))
mockLog.mockReset()
const { executeHttpHook } = await importFreshExecuteHttpHook() 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, "{}") 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(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 () => { 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 () => { it("#when hook uses plain remote http:// URL #then writes warning log", async () => {
mock.module("../../shared/logger", () => ({ // given
log: mockLog, logSpy = spyOn(sharedLogger, "log").mockImplementation(() => {})
}))
const { executeHttpHook } = await importFreshExecuteHttpHook() 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, "{}") await executeHttpHook(hook, "{}")
expect(mockLog).toHaveBeenCalledWith("HTTP hook URL uses insecure protocol", { // then
url: "http://example.com/hooks", 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 () => { it("#when hook uses http://[::1] #then allows execution", async () => {
+12 -6
View File
@@ -10,7 +10,10 @@ describe("createBuiltinMcps", () => {
const result = createBuiltinMcps(disabledMcps) const result = createBuiltinMcps(disabledMcps)
// then // 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", () => { test("should filter out disabled MCPs", () => {
@@ -21,20 +24,23 @@ describe("createBuiltinMcps", () => {
const result = createBuiltinMcps(disabledMcps) const result = createBuiltinMcps(disabledMcps)
// then // 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", () => { test("should return empty array when all MCPs are disabled", () => {
// given - disable all known MCPs // given - disable all known MCPs
const disabledMcps = ["websearch", "context7", "grep-app"] const disabledMcps = ["websearch", "context7", "grep_app"]
// when // when
const result = createBuiltinMcps(disabledMcps) const result = createBuiltinMcps(disabledMcps)
// then - may still have MCPs we didn't list // 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("websearch")
expect(remainingMcpNames).not.toContain("context7") expect(remainingMcpNames).not.toContain("context7")
expect(remainingMcpNames).not.toContain("grep-app") expect(remainingMcpNames).not.toContain("grep_app")
expect(remainingMcpNames).toEqual([])
}) })
}) })
@@ -1,14 +1,14 @@
declare const require: (name: string) => any declare const require: (name: string) => any
const { describe, test, expect, beforeEach, afterEach, spyOn, mock } = require("bun:test") const { describe, test, expect, beforeEach, afterEach, spyOn, mock } = require("bun:test")
import type { DelegateTaskArgs } from "../../types" import type { DelegateTaskArgs } from "../types"
import type { ExecutorContext } from "../../executor-types" import type { ExecutorContext } from "../executor-types"
import * as logger from "../../../../shared/logger" import * as logger from "../../../shared/logger"
import * as connectedProvidersCache from "../../../../shared/connected-providers-cache" import * as connectedProvidersCache from "../../../shared/connected-providers-cache"
type SubagentResolverModule = typeof import("../../subagent-resolver") type SubagentResolverModule = typeof import("../subagent-resolver")
async function importFreshSubagentResolverModule(): Promise<SubagentResolverModule> { async function importFreshSubagentResolverModule(): Promise<SubagentResolverModule> {
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>): DelegateTaskArgs { function createBaseArgs(overrides?: Partial<DelegateTaskArgs>): DelegateTaskArgs {
@@ -8,7 +8,9 @@ import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js"
const originalReadFileSync = fs.readFileSync.bind(fs) const originalReadFileSync = fs.readFileSync.bind(fs)
async function importFreshSkillToolModule(): Promise<typeof import("./tools")> { let createSkillTool: typeof import("../tools").createSkillTool
beforeEach(async () => {
mock.module("node:fs", () => ({ mock.module("node:fs", () => ({
...fs, ...fs,
readFileSync: (path: string, encoding?: string) => { readFileSync: (path: string, encoding?: string) => {
@@ -21,13 +23,10 @@ Test skill body content`
return originalReadFileSync(path, encoding as BufferEncoding) return originalReadFileSync(path, encoding as BufferEncoding)
}, },
})) }))
const module = await import(`./tools?test=${Date.now()}-${Math.random()}`) const module = await import("../tools")
mock.restore() createSkillTool = module.createSkillTool
return module })
}
const { createSkillTool } = await importFreshSkillToolModule()
afterAll(() => { afterAll(() => {
mock.restore() mock.restore()