feat: 添加工具类增强与状态管理改进

- 新增 workflowRuns、remoteTriggerAudit、pipeStatus 等工具
- 增强 permissionSetup: auto mode 和 bypass permissions 始终可用
- 新增多组测试覆盖 (modifiers, teamDiscovery, deepLink 等)
- 修复 parseInt 缺少 radix 参数
- 移除多余 biome-ignore 注释

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
unraid
2026-04-22 22:38:10 +08:00
parent 94c4b37eed
commit fb41513b32
54 changed files with 1037 additions and 102 deletions

View File

@@ -0,0 +1,96 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
let nativePrewarmCalls = 0
let nativeReturnValue = false
let nativeShouldThrow = false
const nativeIsModifierPressed = mock((modifier: string) => {
if (nativeShouldThrow) {
throw new Error('native modifier failure')
}
return nativeReturnValue
})
mock.module('modifiers-napi', () => ({
prewarm: async () => {
nativePrewarmCalls++
},
isModifierPressed: nativeIsModifierPressed,
}))
const originalPlatform = process.platform
async function loadModule() {
return import(`../modifiers.ts?case=${Math.random()}`)
}
beforeEach(() => {
nativePrewarmCalls = 0
nativeReturnValue = false
nativeShouldThrow = false
nativeIsModifierPressed.mockClear()
Object.defineProperty(process, 'platform', {
value: originalPlatform,
configurable: true,
})
})
afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
configurable: true,
})
})
describe('src/utils/modifiers', () => {
test('does not touch the native module on non-darwin', async () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
configurable: true,
})
const mod = await loadModule()
mod.prewarmModifiers()
expect(nativePrewarmCalls).toBe(0)
expect(mod.isModifierPressed('shift')).toBe(false)
expect(nativeIsModifierPressed).not.toHaveBeenCalled()
})
test('caches native prewarm after the first darwin call', async () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
})
const mod = await loadModule()
mod.prewarmModifiers()
mod.prewarmModifiers()
// prewarm is fire-and-forget async — flush microtasks
await new Promise(resolve => setTimeout(resolve, 0))
expect(nativePrewarmCalls).toBe(1)
})
test('forwards modifier checks to the native module on darwin', async () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
})
nativeReturnValue = true
const mod = await loadModule()
expect(mod.isModifierPressed('shift')).toBe(true)
expect(nativeIsModifierPressed).toHaveBeenCalledWith('shift')
})
test('returns false when native modifier checks throw on darwin', async () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
})
nativeShouldThrow = true
const mod = await loadModule()
expect(mod.isModifierPressed('shift')).toBe(false)
})
})

View File

@@ -0,0 +1,69 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { writeRegistry } from '../pipeRegistry'
import { formatPipeRegistryStatus } from '../pipeStatus'
let tempDir: string
let previousConfigDir: string | undefined
beforeEach(() => {
previousConfigDir = process.env.CLAUDE_CONFIG_DIR
tempDir = join(
tmpdir(),
`pipe-status-${Date.now()}-${Math.random().toString(16).slice(2)}`,
)
process.env.CLAUDE_CONFIG_DIR = tempDir
})
afterEach(async () => {
if (previousConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR
} else {
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
}
await rm(tempDir, { recursive: true, force: true })
})
describe('pipe status', () => {
test('formats registry main and sub pipe communication state', async () => {
await writeRegistry({
version: 1,
mainMachineId: 'machine-main-123456',
main: {
id: 'main-id',
pid: 123,
machineId: 'machine-main-123456',
startedAt: 1,
ip: '127.0.0.1',
mac: '00:11:22:33:44:55',
hostname: 'main-host',
pipeName: 'main-pipe',
tcpPort: 43123,
},
subs: [
{
id: 'sub-id',
pid: 456,
machineId: 'machine-sub-123456',
startedAt: 2,
ip: '127.0.0.2',
mac: '66:77:88:99:aa:bb',
hostname: 'sub-host',
pipeName: 'sub-pipe',
tcpPort: 43124,
subIndex: 1,
boundToMain: 'main-pipe',
},
],
})
const formatted = await formatPipeRegistryStatus()
expect(formatted).toContain('Pipe registry: 1 main, 1 sub(s)')
expect(formatted).toContain('[main] main-pipe')
expect(formatted).toContain('[sub-1] sub-pipe')
expect(formatted).toContain('bound=main-pipe')
})
})

View File

@@ -0,0 +1,37 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { formatRemoteControlLocalStatus } from '../remoteControlStatus'
let previousBaseUrl: string | undefined
let previousToken: string | undefined
beforeEach(() => {
previousBaseUrl = process.env.CLAUDE_BRIDGE_BASE_URL
previousToken = process.env.CLAUDE_BRIDGE_OAUTH_TOKEN
})
afterEach(() => {
if (previousBaseUrl === undefined) {
delete process.env.CLAUDE_BRIDGE_BASE_URL
} else {
process.env.CLAUDE_BRIDGE_BASE_URL = previousBaseUrl
}
if (previousToken === undefined) {
delete process.env.CLAUDE_BRIDGE_OAUTH_TOKEN
} else {
process.env.CLAUDE_BRIDGE_OAUTH_TOKEN = previousToken
}
})
describe('remote control status', () => {
test('formats self-hosted bridge local config without remote calls', () => {
process.env.CLAUDE_BRIDGE_BASE_URL = 'http://127.0.0.1:8787'
process.env.CLAUDE_BRIDGE_OAUTH_TOKEN = 'token'
const status = formatRemoteControlLocalStatus()
expect(status).toContain('Remote Control: self-hosted')
expect(status).toContain('base_url=http://127.0.0.1:8787')
expect(status).toContain('token=present')
expect(status).toContain('entitlement=checked at remote-control startup')
})
})

View File

@@ -0,0 +1,43 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import {
appendRemoteTriggerAuditRecord,
formatRemoteTriggerAuditStatus,
listRemoteTriggerAuditRecords,
} from '../remoteTriggerAudit'
let tempDir = ''
beforeEach(() => {
tempDir = join(
tmpdir(),
`remote-trigger-audit-${Date.now()}-${Math.random().toString(16).slice(2)}`,
)
})
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true })
})
describe('remote trigger audit', () => {
test('records and formats local remote trigger audit events', async () => {
await appendRemoteTriggerAuditRecord(
{ action: 'run', triggerId: 'abc', ok: true, status: 200, createdAt: 1 },
tempDir,
)
await appendRemoteTriggerAuditRecord(
{ action: 'create', ok: false, error: 'bad request', createdAt: 2 },
tempDir,
)
const records = await listRemoteTriggerAuditRecords(tempDir)
expect(records).toHaveLength(2)
expect(records[0].action).toBe('create')
expect(formatRemoteTriggerAuditStatus(records)).toContain(
'RemoteTrigger audit records: 2',
)
expect(formatRemoteTriggerAuditStatus(records)).toContain('Failures: 1')
})
})

View File

@@ -0,0 +1,68 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { getTeammateStatuses } from '../teamDiscovery'
let tempHome: string
let previousConfigDir: string | undefined
beforeEach(() => {
previousConfigDir = process.env.CLAUDE_CONFIG_DIR
tempHome = join(
tmpdir(),
`team-discovery-${Date.now()}-${Math.random().toString(16).slice(2)}`,
)
process.env.CLAUDE_CONFIG_DIR = tempHome
})
afterEach(() => {
if (previousConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR
} else {
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
}
rmSync(tempHome, { recursive: true, force: true })
})
function writeTeamConfig(teamName: string, config: unknown): void {
const teamDir = join(tempHome, 'teams', teamName)
mkdirSync(teamDir, { recursive: true })
writeFileSync(join(teamDir, 'config.json'), JSON.stringify(config, null, 2))
}
describe('getTeammateStatuses', () => {
test('preserves in-process backend type for lifecycle actions', () => {
writeTeamConfig('alpha', {
name: 'alpha',
createdAt: Date.now(),
leadAgentId: 'team-lead@alpha',
members: [
{
agentId: 'team-lead@alpha',
name: 'team-lead',
joinedAt: Date.now(),
tmuxPaneId: '',
cwd: tempHome,
subscriptions: [],
},
{
agentId: 'worker@alpha',
name: 'worker',
joinedAt: Date.now(),
tmuxPaneId: 'in-process',
cwd: tempHome,
subscriptions: [],
backendType: 'in-process',
},
],
})
expect(getTeammateStatuses('alpha')).toEqual([
expect.objectContaining({
agentId: 'worker@alpha',
backendType: 'in-process',
}),
])
})
})

View File

@@ -30,6 +30,18 @@ mock.module("src/services/tokenEstimation.ts", () => ({
countTokensViaHaikuFallback: async () => 0,
}));
// Mock slowOperations to avoid bun:bundle import
mock.module('src/utils/slowOperations.ts', () => ({
jsonStringify: JSON.stringify,
jsonParse: JSON.parse,
slowLogging: { enabled: false },
clone: (v: any) => structuredClone(v),
cloneDeep: (v: any) => structuredClone(v),
callerFrame: () => '',
SLOW_OPERATION_THRESHOLD_MS: 100,
writeFileSync_DEPRECATED: () => {},
}))
const {
getTokenCountFromUsage,
getTokenUsage,