From b10a467dc6c0492905b47bef66f6f3c1c67019b7 Mon Sep 17 00:00:00 2001 From: Im10furry Date: Fri, 5 Jun 2026 21:49:58 +0800 Subject: [PATCH 1/2] style: format code with prettier --- src/tools/system/BashTool/llmSafetyGate.ts | 16 +++++- tests/unit/bash-llm-gate.test.ts | 61 ++++++++++------------ tests/unit/tools/tools-basic.test.ts | 22 +++++++- 3 files changed, 64 insertions(+), 35 deletions(-) diff --git a/src/tools/system/BashTool/llmSafetyGate.ts b/src/tools/system/BashTool/llmSafetyGate.ts index b68a42bed..4c02605a0 100644 --- a/src/tools/system/BashTool/llmSafetyGate.ts +++ b/src/tools/system/BashTool/llmSafetyGate.ts @@ -125,6 +125,19 @@ type GateQueryFn = (args: { model?: 'quick' | 'main' }) => Promise +type GateLlmModule = { + API_ERROR_MESSAGE_PREFIX: string + queryLLM: typeof import('@services/llm').queryLLM +} + +let defaultGateQueryLlmModuleForTests: GateLlmModule | null = null + +export function __setDefaultGateQueryLlmModuleForTests( + llmModule: GateLlmModule | null, +): void { + defaultGateQueryLlmModuleForTests = llmModule +} + function collectTextBlocks(content: any): string { if (typeof content === 'string') return content if (!Array.isArray(content)) return '' @@ -159,7 +172,8 @@ async function defaultGateQuery(args: { signal: AbortSignal model?: 'quick' | 'main' }): Promise { - const { API_ERROR_MESSAGE_PREFIX, queryLLM } = await import('@services/llm') + const { API_ERROR_MESSAGE_PREFIX, queryLLM } = + defaultGateQueryLlmModuleForTests ?? (await import('@services/llm')) const messages: any[] = [ { type: 'user', diff --git a/tests/unit/bash-llm-gate.test.ts b/tests/unit/bash-llm-gate.test.ts index c7d682385..0c82b83a2 100644 --- a/tests/unit/bash-llm-gate.test.ts +++ b/tests/unit/bash-llm-gate.test.ts @@ -1,5 +1,6 @@ -import { describe, expect, mock, test } from 'bun:test' +import { describe, expect, test } from 'bun:test' import { + __setDefaultGateQueryLlmModuleForTests, formatBashLlmGateBlockMessage, runBashLlmSafetyGate, } from '@tools/BashTool/llmSafetyGate' @@ -158,23 +159,21 @@ describe('Bash LLM intent gate', () => { test('uses defaultGateQuery (mocked) when no query is provided', async () => { try { - mock.module('@services/llm', () => { - return { - queryLLM: async () => { - return { - message: { - content: [ - { type: 'not_text', text: 'ignored' }, - { - type: 'text', - text: 'ALLOW', - }, - ], - }, - } - }, - API_ERROR_MESSAGE_PREFIX: 'API_ERROR: ', - } + __setDefaultGateQueryLlmModuleForTests({ + queryLLM: async () => { + return { + message: { + content: [ + { type: 'not_text', text: 'ignored' }, + { + type: 'text', + text: 'ALLOW', + }, + ], + }, + } as any + }, + API_ERROR_MESSAGE_PREFIX: 'API_ERROR: ', }) const result = await runBashLlmSafetyGate({ @@ -192,24 +191,22 @@ describe('Bash LLM intent gate', () => { }) expect(result.decision).toBe('allow') } finally { - mock.restore() + __setDefaultGateQueryLlmModuleForTests(null) } }) test('defaultGateQuery surfaces API error messages as gate errors', async () => { try { - mock.module('@services/llm', () => { - return { - queryLLM: async () => { - return { - isApiErrorMessage: true, - message: { - content: [{ type: 'text', text: 'API_ERROR: Invalid API key' }], - }, - } - }, - API_ERROR_MESSAGE_PREFIX: 'API_ERROR: ', - } + __setDefaultGateQueryLlmModuleForTests({ + queryLLM: async () => { + return { + isApiErrorMessage: true, + message: { + content: [{ type: 'text', text: 'API_ERROR: Invalid API key' }], + }, + } as any + }, + API_ERROR_MESSAGE_PREFIX: 'API_ERROR: ', }) const result = await runBashLlmSafetyGate({ @@ -230,7 +227,7 @@ describe('Bash LLM intent gate', () => { expect(result.error).toContain('LLM gate model error:') } } finally { - mock.restore() + __setDefaultGateQueryLlmModuleForTests(null) } }) diff --git a/tests/unit/tools/tools-basic.test.ts b/tests/unit/tools/tools-basic.test.ts index a80b6bbd5..6c552c6f0 100644 --- a/tests/unit/tools/tools-basic.test.ts +++ b/tests/unit/tools/tools-basic.test.ts @@ -137,6 +137,24 @@ describe('Plan mode gating', () => { }) describe('Bash background execution', () => { + async function waitForBackgroundStdout( + bashId: string, + expected: string[], + ): Promise { + let lastOutput: ReturnType = null + for (let i = 0; i < 40; i++) { + const output = BunShell.getInstance().getBackgroundOutput(bashId) + lastOutput = output + if (output && expected.every(value => output.stdout.includes(value))) { + return + } + await new Promise(resolve => setTimeout(resolve, 50)) + } + throw new Error( + `Timed out waiting for background stdout: ${JSON.stringify(lastOutput)}`, + ) + } + test('executes background command and reports output', async () => { const { bashId } = BunShell.getInstance().execInBackground('echo hello') expect(bashId).toBeTruthy() @@ -151,10 +169,10 @@ describe('Bash background execution', () => { test('readBackgroundOutput returns only new output', async () => { const { bashId } = - BunShell.getInstance().execInBackground('printf "a\\nb\\n"') + BunShell.getInstance().execInBackground('echo a && echo b') expect(bashId).toBeTruthy() expect(bashId).toMatch(/^b[0-9a-f]{6}$/i) - await new Promise(resolve => setTimeout(resolve, 200)) + await waitForBackgroundStdout(bashId, ['a', 'b']) const first = BunShell.getInstance().readBackgroundOutput(bashId) expect(first).not.toBeNull() From 010bcbcfa10be6b5d71000e492eb67d6a1fa9e84 Mon Sep 17 00:00:00 2001 From: Im10furry Date: Sat, 6 Jun 2026 16:28:17 +0800 Subject: [PATCH 2/2] test: stabilize Windows permission and LSP gates --- src/utils/permissions/fileToolPermissionEngine.ts | 1 + tests/unit/lsp-tool.test.ts | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/utils/permissions/fileToolPermissionEngine.ts b/src/utils/permissions/fileToolPermissionEngine.ts index 84eade926..53fcc8e9c 100644 --- a/src/utils/permissions/fileToolPermissionEngine.ts +++ b/src/utils/permissions/fileToolPermissionEngine.ts @@ -116,6 +116,7 @@ function posixRelative(fromPath: string, toPath: string): string { export function expandSymlinkPaths(inputPath: string): string[] { const out = [inputPath] + if (inputPath.startsWith('\\\\') || inputPath.startsWith('//')) return out if (!existsSync(inputPath)) return out try { const resolved = realpathSync(inputPath) diff --git a/tests/unit/lsp-tool.test.ts b/tests/unit/lsp-tool.test.ts index 205006883..52068c93c 100644 --- a/tests/unit/lsp-tool.test.ts +++ b/tests/unit/lsp-tool.test.ts @@ -1,10 +1,19 @@ -import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import { + afterEach, + beforeEach, + describe, + expect, + setDefaultTimeout, + test, +} from 'bun:test' import { mkdtempSync, rmSync, statSync, utimesSync, writeFileSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' import { LspTool } from '@tools/search/LspTool/LspTool' import { setCwd } from '@utils/state' +setDefaultTimeout(15_000) + function makeContext(): any { return { abortController: new AbortController(),