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:
- 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
+2 -2
View File
@@ -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=="],
+1 -1
View File
@@ -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": {
@@ -1,13 +1,12 @@
/// <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 * 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<typeof spyOn> | 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 () => {
+12 -6
View File
@@ -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([])
})
})
})
@@ -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<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 {
@@ -8,7 +8,9 @@ import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js"
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", () => ({
...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()