test: fix CI test isolation - upgrade Bun, fix mock contamination and fresh-import patterns
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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 () => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user