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:
|
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
|
||||||
|
|||||||
@@ -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
@@ -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 () => {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
@@ -22,12 +24,9 @@ Test skill body content`
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user