mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
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:
96
src/utils/__tests__/modifiers.test.ts
Normal file
96
src/utils/__tests__/modifiers.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
69
src/utils/__tests__/pipeStatus.test.ts
Normal file
69
src/utils/__tests__/pipeStatus.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
37
src/utils/__tests__/remoteControlStatus.test.ts
Normal file
37
src/utils/__tests__/remoteControlStatus.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
43
src/utils/__tests__/remoteTriggerAudit.test.ts
Normal file
43
src/utils/__tests__/remoteTriggerAudit.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
68
src/utils/__tests__/teamDiscovery.test.ts
Normal file
68
src/utils/__tests__/teamDiscovery.test.ts
Normal 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',
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user