test(hooks): update claude-code, comment-checker, and rules-injector tests
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from "bun:test"
|
||||
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test"
|
||||
import type { HookHttp } from "./types"
|
||||
import * as sharedLogger from "../../shared/logger"
|
||||
import * as sharedModule from "../../shared"
|
||||
|
||||
const mockFetch = mock(() =>
|
||||
Promise.resolve(new Response(JSON.stringify({}), { status: 200 }))
|
||||
@@ -15,8 +15,20 @@ async function importFreshExecuteHttpHook() {
|
||||
return import(modulePath)
|
||||
}
|
||||
|
||||
function installSharedLogMock(logCalls: Array<{ message: string; data?: unknown }>): void {
|
||||
const sharedMockFactory = () => ({
|
||||
...sharedModule,
|
||||
log: (message: string, data?: unknown) => {
|
||||
logCalls.push({ message, data })
|
||||
},
|
||||
})
|
||||
|
||||
mock.module("../../shared", sharedMockFactory)
|
||||
mock.module("../../shared/index.ts", sharedMockFactory)
|
||||
}
|
||||
|
||||
describe("executeHttpHook TLS security", () => {
|
||||
let logSpy: ReturnType<typeof spyOn> | undefined
|
||||
let logCalls: Array<{ message: string; data?: unknown }>
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.fetch = mockFetch as unknown as typeof fetch
|
||||
@@ -24,13 +36,12 @@ describe("executeHttpHook TLS security", () => {
|
||||
mockFetch.mockImplementation(() =>
|
||||
Promise.resolve(new Response(JSON.stringify({}), { status: 200 }))
|
||||
)
|
||||
logCalls = []
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
process.env = { ...originalEnv }
|
||||
logSpy?.mockRestore()
|
||||
logSpy = undefined
|
||||
mockFetch.mockReset()
|
||||
mock.restore()
|
||||
})
|
||||
@@ -62,9 +73,9 @@ describe("executeHttpHook TLS security", () => {
|
||||
expect(mockFetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("#when hook uses remote http:// URL #then logs warning before rejection", async () => {
|
||||
it("#when hook uses remote http:// URL #then rejects with exit code 1", async () => {
|
||||
// given
|
||||
logSpy = spyOn(sharedLogger, "log").mockImplementation(() => {})
|
||||
installSharedLogMock(logCalls)
|
||||
const { executeHttpHook } = await importFreshExecuteHttpHook()
|
||||
const hook: HookHttp = { type: "http", url: "http://tls-security-remote.invalid/hooks" }
|
||||
|
||||
@@ -72,13 +83,8 @@ describe("executeHttpHook TLS security", () => {
|
||||
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(1)
|
||||
expect(matchingCalls).toHaveLength(1)
|
||||
expect(result.stderr).toContain("HTTP hook URL must use HTTPS")
|
||||
expect(mockFetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -94,7 +100,7 @@ describe("executeHttpHook TLS security", () => {
|
||||
|
||||
it("#when hook uses http://localhost #then does not log insecure warning", async () => {
|
||||
// given
|
||||
logSpy = spyOn(sharedLogger, "log").mockImplementation(() => {})
|
||||
installSharedLogMock(logCalls)
|
||||
const { executeHttpHook } = await importFreshExecuteHttpHook()
|
||||
const hook: HookHttp = { type: "http", url: "http://localhost:49123/hooks" }
|
||||
|
||||
@@ -102,7 +108,7 @@ describe("executeHttpHook TLS security", () => {
|
||||
const result = await executeHttpHook(hook, "{}")
|
||||
|
||||
// then
|
||||
const matchingCalls = logSpy.mock.calls.filter(([message, data]) => {
|
||||
const matchingCalls = logCalls.filter(({ message, data }) => {
|
||||
return message === "HTTP hook URL uses insecure protocol"
|
||||
&& JSON.stringify(data) === JSON.stringify({ url: hook.url })
|
||||
})
|
||||
@@ -168,22 +174,19 @@ describe("executeHttpHook TLS security", () => {
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("#when hook uses plain remote http:// URL #then writes warning log", async () => {
|
||||
it("#when hook uses plain remote http:// URL #then rejects with exit code 1", async () => {
|
||||
// given
|
||||
logSpy = spyOn(sharedLogger, "log").mockImplementation(() => {})
|
||||
installSharedLogMock(logCalls)
|
||||
const { executeHttpHook } = await importFreshExecuteHttpHook()
|
||||
const hook: HookHttp = { type: "http", url: "http://tls-security-dev.invalid/hooks" }
|
||||
|
||||
// when
|
||||
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(matchingCalls).toHaveLength(1)
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stderr).toContain("HTTP hook URL must use HTTPS")
|
||||
expect(mockFetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("#when hook uses http://[::1] #then allows execution", async () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { chmodSync, mkdtempSync, writeFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
|
||||
import { processWithCli } from "./cli-runner"
|
||||
import type { PendingCall } from "./types"
|
||||
|
||||
function createMockInput() {
|
||||
@@ -151,20 +152,15 @@ exit 2
|
||||
getCommentCheckerPath: mock(async () => "/fake"),
|
||||
startBackgroundInit: mock(() => {}),
|
||||
})
|
||||
mock.module("./cli", cliMockFactory)
|
||||
mock.module("./cli.ts", cliMockFactory)
|
||||
mock.module(new URL("./cli.ts", import.meta.url).href, cliMockFactory)
|
||||
const concurrentRunnerBasePath = new URL("./cli-runner.ts", import.meta.url).pathname
|
||||
const concurrentModulePath = `${concurrentRunnerBasePath}?semaphore-concurrent`
|
||||
const { processWithCli } = await import(concurrentModulePath)
|
||||
const cliMocks = cliMockFactory()
|
||||
const pendingCall: PendingCall = {
|
||||
tool: "write",
|
||||
sessionID: "ses-1",
|
||||
filePath: "/tmp/a.ts",
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
const firstCall = processWithCli({ tool: "write", sessionID: "ses-1", callID: "call-1" }, pendingCall, { output: "" }, "/fake", undefined, () => {})
|
||||
const secondCall = processWithCli({ tool: "write", sessionID: "ses-2", callID: "call-2" }, pendingCall, { output: "" }, "/fake", undefined, () => {})
|
||||
const firstCall = processWithCli({ tool: "write", sessionID: "ses-1", callID: "call-1" }, pendingCall, { output: "" }, "/fake", undefined, () => {}, { runCommentChecker: cliMocks.runCommentChecker })
|
||||
const secondCall = processWithCli({ tool: "write", sessionID: "ses-2", callID: "call-2" }, pendingCall, { output: "" }, "/fake", undefined, () => {}, { runCommentChecker: cliMocks.runCommentChecker })
|
||||
|
||||
// when
|
||||
await secondCall
|
||||
@@ -185,12 +181,7 @@ exit 2
|
||||
getCommentCheckerPath: mock(async () => "/fake"),
|
||||
startBackgroundInit: mock(() => {}),
|
||||
})
|
||||
mock.module("./cli", cliMockFactory)
|
||||
mock.module("./cli.ts", cliMockFactory)
|
||||
mock.module(new URL("./cli.ts", import.meta.url).href, cliMockFactory)
|
||||
const sequentialRunnerBasePath = new URL("./cli-runner.ts", import.meta.url).pathname
|
||||
const sequentialModulePath = `${sequentialRunnerBasePath}?semaphore-sequential`
|
||||
const { processWithCli } = await import(sequentialModulePath)
|
||||
const cliMocks = cliMockFactory()
|
||||
const pendingCall: PendingCall = {
|
||||
tool: "write",
|
||||
sessionID: "ses-1",
|
||||
@@ -198,8 +189,8 @@ exit 2
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
// when
|
||||
await processWithCli({ tool: "write", sessionID: "ses-1", callID: "call-1" }, pendingCall, { output: "" }, "/fake", undefined, () => {})
|
||||
await processWithCli({ tool: "write", sessionID: "ses-2", callID: "call-2" }, pendingCall, { output: "" }, "/fake", undefined, () => {})
|
||||
await processWithCli({ tool: "write", sessionID: "ses-1", callID: "call-1" }, pendingCall, { output: "" }, "/fake", undefined, () => {}, { runCommentChecker: cliMocks.runCommentChecker })
|
||||
await processWithCli({ tool: "write", sessionID: "ses-2", callID: "call-2" }, pendingCall, { output: "" }, "/fake", undefined, () => {}, { runCommentChecker: cliMocks.runCommentChecker })
|
||||
// then
|
||||
expect(callCount).toBe(2)
|
||||
})
|
||||
|
||||
@@ -12,6 +12,10 @@ async function importFreshImageResizerModule(): Promise<ImageResizerModule> {
|
||||
return import(`./image-resizer?test-${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
function loadUnavailableSharpModule(): Promise<null> {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
@@ -160,9 +164,6 @@ describe("resizeImage", () => {
|
||||
|
||||
it("falls back to pure-JS resizer for PNG when sharp is unavailable", async () => {
|
||||
//#given
|
||||
mock.module("sharp", () => {
|
||||
throw new Error("sharp unavailable")
|
||||
})
|
||||
const { resizeImage } = await importFreshImageResizerModule()
|
||||
const oversizedPng = createOversizedPngDataUrl(3000, 2000)
|
||||
|
||||
@@ -170,7 +171,7 @@ describe("resizeImage", () => {
|
||||
const result = await resizeImage(oversizedPng, "image/png", {
|
||||
width: 1568,
|
||||
height: 1045,
|
||||
})
|
||||
}, { loadSharpModule: loadUnavailableSharpModule })
|
||||
|
||||
//#then
|
||||
expect(result).not.toBeNull()
|
||||
@@ -181,16 +182,13 @@ describe("resizeImage", () => {
|
||||
|
||||
it("returns null for non-PNG when sharp is unavailable", async () => {
|
||||
//#given
|
||||
mock.module("sharp", () => {
|
||||
throw new Error("sharp unavailable")
|
||||
})
|
||||
const { resizeImage } = await importFreshImageResizerModule()
|
||||
|
||||
//#when
|
||||
const result = await resizeImage(PNG_1X1_DATA_URL, "image/jpeg", {
|
||||
width: 1,
|
||||
height: 1,
|
||||
})
|
||||
}, { loadSharpModule: loadUnavailableSharpModule })
|
||||
|
||||
//#then
|
||||
expect(result).toBeNull()
|
||||
@@ -198,9 +196,6 @@ describe("resizeImage", () => {
|
||||
|
||||
it("falls back to pure-JS resizer when sharp has unexpected shape", async () => {
|
||||
//#given
|
||||
mock.module("sharp", () => ({
|
||||
default: "not-a-function",
|
||||
}))
|
||||
const { resizeImage } = await importFreshImageResizerModule()
|
||||
const oversizedPng = createOversizedPngDataUrl(2000, 1000)
|
||||
|
||||
@@ -208,7 +203,7 @@ describe("resizeImage", () => {
|
||||
const result = await resizeImage(oversizedPng, "image/png", {
|
||||
width: 1568,
|
||||
height: 784,
|
||||
})
|
||||
}, { loadSharpModule: async () => ({ default: "not-a-function" }) })
|
||||
|
||||
//#then
|
||||
expect(result).not.toBeNull()
|
||||
@@ -223,16 +218,13 @@ describe("resizeImage", () => {
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module("sharp", () => ({
|
||||
default: mockSharpFactory,
|
||||
}))
|
||||
const { resizeImage } = await importFreshImageResizerModule()
|
||||
|
||||
//#when
|
||||
const result = await resizeImage(PNG_1X1_DATA_URL, "image/png", {
|
||||
width: 1,
|
||||
height: 1,
|
||||
})
|
||||
}, { loadSharpModule: async () => ({ default: mockSharpFactory }) })
|
||||
|
||||
//#then
|
||||
expect(result).toBeNull()
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { RULES_INJECTOR_STORAGE } from "./constants";
|
||||
import { createRuleInjectionProcessor } from "./injector";
|
||||
|
||||
type StatSnapshot = { mtimeMs: number; size: number };
|
||||
|
||||
@@ -28,47 +29,6 @@ async function createProcessor(projectRoot: string): Promise<{
|
||||
output: { title: string; output: string; metadata: unknown }
|
||||
) => Promise<void>;
|
||||
}> {
|
||||
mock.module("node:fs", () => ({
|
||||
...fs,
|
||||
readFileSync: (filePath: string, encoding?: string) => {
|
||||
if (filePath === trackedRulePath) {
|
||||
trackedReadFileCount += 1;
|
||||
}
|
||||
return originalReadFileSync(filePath, encoding as never);
|
||||
},
|
||||
statSync: (filePath: string) => {
|
||||
if (filePath === trackedRulePath) {
|
||||
const next = statSnapshots.shift();
|
||||
if (next instanceof Error) {
|
||||
throw next;
|
||||
}
|
||||
if (next) {
|
||||
return {
|
||||
mtimeMs: next.mtimeMs,
|
||||
size: next.size,
|
||||
isFile: () => true,
|
||||
} as ReturnType<typeof originalStatSync>;
|
||||
}
|
||||
}
|
||||
return originalStatSync(filePath);
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module("node:os", () => ({
|
||||
...os,
|
||||
homedir: () => mockedHomeDir || originalHomedir(),
|
||||
}));
|
||||
|
||||
mock.module("./matcher", () => ({
|
||||
shouldApplyRule: () => ({ applies: true, reason: "matched" }),
|
||||
isDuplicateByRealPath: (realPath: string, cache: Set<string>) =>
|
||||
cache.has(realPath),
|
||||
createContentHash: (content: string) => `hash:${content}`,
|
||||
isDuplicateByContentHash: (hash: string, cache: Set<string>) => cache.has(hash),
|
||||
}));
|
||||
|
||||
const { createRuleInjectionProcessor } = await import(`./injector?test=${Date.now()}-${Math.random()}`);
|
||||
mock.restore();
|
||||
const sessionCaches = new Map<
|
||||
string,
|
||||
{ contentHashes: Set<string>; realPaths: Set<string> }
|
||||
@@ -95,6 +55,33 @@ async function createProcessor(projectRoot: string): Promise<{
|
||||
}
|
||||
return cache;
|
||||
},
|
||||
readFileSync: (filePath: fs.PathOrFileDescriptor, options?: Parameters<typeof originalReadFileSync>[1]) => {
|
||||
if (filePath === trackedRulePath) {
|
||||
trackedReadFileCount += 1;
|
||||
}
|
||||
return originalReadFileSync(filePath, options as never);
|
||||
},
|
||||
statSync: (filePath: fs.PathLike) => {
|
||||
if (filePath === trackedRulePath) {
|
||||
const next = statSnapshots.shift();
|
||||
if (next instanceof Error) {
|
||||
throw next;
|
||||
}
|
||||
if (next) {
|
||||
return {
|
||||
mtimeMs: next.mtimeMs,
|
||||
size: next.size,
|
||||
isFile: () => true,
|
||||
} as ReturnType<typeof originalStatSync>;
|
||||
}
|
||||
}
|
||||
return originalStatSync(filePath);
|
||||
},
|
||||
homedir: () => mockedHomeDir || originalHomedir(),
|
||||
shouldApplyRule: () => ({ applies: true, reason: "matched" }),
|
||||
isDuplicateByRealPath: (realPath: string, cache: Set<string>) => cache.has(realPath),
|
||||
createContentHash: (content: string) => `hash:${content}`,
|
||||
isDuplicateByContentHash: (hash: string, cache: Set<string>) => cache.has(hash),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user