mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
feat(remote-control): 优化 Web 展示、状态同步与桥接控制流程 (#288)
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
This commit is contained in:
96
src/bridge/__tests__/bridgeMessaging.test.ts
Normal file
96
src/bridge/__tests__/bridgeMessaging.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
shouldReportRunningForMessage,
|
||||
shouldReportRunningForMessages,
|
||||
} from '../bridgeMessaging.js'
|
||||
import { createUserMessage } from '../../utils/messages.js'
|
||||
|
||||
describe('bridge running-state classification', () => {
|
||||
test('treats real user prompts as turn-starting work', () => {
|
||||
expect(
|
||||
shouldReportRunningForMessage(
|
||||
createUserMessage({ content: 'please inspect the repo' }),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('keeps tool-result style user messages eligible during mid-turn attach', () => {
|
||||
expect(
|
||||
shouldReportRunningForMessage(
|
||||
createUserMessage({
|
||||
content: '<local-command-stdout>done</local-command-stdout>',
|
||||
toolUseResult: { ok: true },
|
||||
}),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('ignores local slash-command scaffolding that should not reopen a turn', () => {
|
||||
expect(
|
||||
shouldReportRunningForMessage(
|
||||
createUserMessage({
|
||||
content:
|
||||
'<local-command-caveat>Caveat: hidden local command scaffolding</local-command-caveat>',
|
||||
isMeta: true,
|
||||
}),
|
||||
),
|
||||
).toBe(false)
|
||||
|
||||
expect(
|
||||
shouldReportRunningForMessage(
|
||||
createUserMessage({
|
||||
content:
|
||||
'<system-reminder>\nProactive mode is now enabled. You will receive periodic <tick> prompts.\n</system-reminder>',
|
||||
isMeta: true,
|
||||
}),
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test('still marks real automation triggers as running', () => {
|
||||
expect(
|
||||
shouldReportRunningForMessage(
|
||||
createUserMessage({
|
||||
content: '<tick>2:56:47 PM</tick>',
|
||||
isMeta: true,
|
||||
}),
|
||||
),
|
||||
).toBe(true)
|
||||
|
||||
expect(
|
||||
shouldReportRunningForMessage(
|
||||
createUserMessage({
|
||||
content: 'scheduled job: refresh analytics cache',
|
||||
isMeta: true,
|
||||
}),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('classifies batches by any work-starting message', () => {
|
||||
const scaffoldingOnly = [
|
||||
createUserMessage({
|
||||
content:
|
||||
'<local-command-caveat>Caveat: hidden local command scaffolding</local-command-caveat>',
|
||||
isMeta: true,
|
||||
}),
|
||||
createUserMessage({
|
||||
content:
|
||||
'<system-reminder>\nProactive mode is now enabled.\n</system-reminder>',
|
||||
isMeta: true,
|
||||
}),
|
||||
]
|
||||
expect(shouldReportRunningForMessages(scaffoldingOnly)).toBe(false)
|
||||
|
||||
expect(
|
||||
shouldReportRunningForMessages([
|
||||
...scaffoldingOnly,
|
||||
createUserMessage({
|
||||
content: '<tick>2:57:17 PM</tick>',
|
||||
isMeta: true,
|
||||
}),
|
||||
]),
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
76
src/bridge/__tests__/bridgePermissionCallbacks.test.ts
Normal file
76
src/bridge/__tests__/bridgePermissionCallbacks.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { parseBridgePermissionResponse } from '../bridgePermissionCallbacks.js'
|
||||
import type { SDKControlResponse } from '../../entrypoints/sdk/controlTypes.js'
|
||||
|
||||
describe('parseBridgePermissionResponse', () => {
|
||||
test('passes through allow responses', () => {
|
||||
const message: SDKControlResponse = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'success',
|
||||
request_id: 'req-1',
|
||||
response: {
|
||||
behavior: 'allow',
|
||||
updatedPermissions: [
|
||||
{ type: 'setMode', mode: 'acceptEdits', destination: 'session' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect(parseBridgePermissionResponse(message)).toEqual({
|
||||
behavior: 'allow',
|
||||
updatedPermissions: [
|
||||
{ type: 'setMode', mode: 'acceptEdits', destination: 'session' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test('maps error responses with feedback to deny', () => {
|
||||
const message = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'error',
|
||||
request_id: 'req-2',
|
||||
error: 'Permission denied by user',
|
||||
response: { behavior: 'deny' },
|
||||
message: 'Need more detail',
|
||||
},
|
||||
} as unknown as SDKControlResponse
|
||||
|
||||
expect(parseBridgePermissionResponse(message)).toEqual({
|
||||
behavior: 'deny',
|
||||
message: 'Need more detail',
|
||||
})
|
||||
})
|
||||
|
||||
test('falls back to error text when deny feedback is absent', () => {
|
||||
const message = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'error',
|
||||
request_id: 'req-3',
|
||||
error: 'Permission denied by user',
|
||||
},
|
||||
} as unknown as SDKControlResponse
|
||||
|
||||
expect(parseBridgePermissionResponse(message)).toEqual({
|
||||
behavior: 'deny',
|
||||
message: 'Permission denied by user',
|
||||
})
|
||||
})
|
||||
|
||||
test('returns null for unrelated control responses', () => {
|
||||
const message = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'error',
|
||||
request_id: 'req-4',
|
||||
error: '',
|
||||
},
|
||||
} as unknown as SDKControlResponse
|
||||
|
||||
expect(parseBridgePermissionResponse(message)).toBeNull()
|
||||
})
|
||||
})
|
||||
53
src/bridge/__tests__/bridgeResultScheduling.test.ts
Normal file
53
src/bridge/__tests__/bridgeResultScheduling.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
hasPendingBridgeMessages,
|
||||
isTranscriptResetResultReady,
|
||||
shouldDeferBridgeResult,
|
||||
} from '../bridgeResultScheduling.js'
|
||||
|
||||
describe('bridgeResultScheduling', () => {
|
||||
test('detects pending mirrored messages', () => {
|
||||
expect(hasPendingBridgeMessages(2, 3)).toBe(true)
|
||||
expect(hasPendingBridgeMessages(3, 3)).toBe(false)
|
||||
})
|
||||
|
||||
test('defers when the bridge handle is unavailable', () => {
|
||||
expect(
|
||||
shouldDeferBridgeResult({
|
||||
hasHandle: false,
|
||||
isConnected: true,
|
||||
lastWrittenIndex: 3,
|
||||
messageCount: 3,
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('defers when the bridge is connected but transcript flush is pending', () => {
|
||||
expect(
|
||||
shouldDeferBridgeResult({
|
||||
hasHandle: true,
|
||||
isConnected: true,
|
||||
lastWrittenIndex: 1,
|
||||
messageCount: 2,
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('sends immediately once the latest transcript is already mirrored', () => {
|
||||
expect(
|
||||
shouldDeferBridgeResult({
|
||||
hasHandle: true,
|
||||
isConnected: true,
|
||||
lastWrittenIndex: 2,
|
||||
messageCount: 2,
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test('treats transcript reset as ready only after the transcript is empty', () => {
|
||||
expect(isTranscriptResetResultReady(true, 0)).toBe(true)
|
||||
expect(isTranscriptResetResultReady(true, 1)).toBe(false)
|
||||
expect(isTranscriptResetResultReady(false, 0)).toBe(false)
|
||||
})
|
||||
})
|
||||
37
src/bridge/__tests__/remoteInterruptHandling.test.ts
Normal file
37
src/bridge/__tests__/remoteInterruptHandling.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { afterEach, describe, expect, test } from 'bun:test'
|
||||
|
||||
import { handleRemoteInterrupt } from '../remoteInterruptHandling.js'
|
||||
import {
|
||||
activateProactive,
|
||||
deactivateProactive,
|
||||
isProactivePaused,
|
||||
} from '../../proactive/index.js'
|
||||
|
||||
function isProactiveFeatureEnabled() {
|
||||
if (feature('PROACTIVE')) return true
|
||||
return feature('KAIROS') ? true : false
|
||||
}
|
||||
|
||||
describe('handleRemoteInterrupt', () => {
|
||||
afterEach(() => {
|
||||
deactivateProactive()
|
||||
})
|
||||
|
||||
test('always aborts the active request', () => {
|
||||
const controller = new AbortController()
|
||||
|
||||
handleRemoteInterrupt(controller)
|
||||
|
||||
expect(controller.signal.aborted).toBe(true)
|
||||
})
|
||||
|
||||
test('pauses proactive mode to return control to the user', () => {
|
||||
activateProactive('test')
|
||||
expect(isProactivePaused()).toBe(false)
|
||||
|
||||
handleRemoteInterrupt(new AbortController())
|
||||
|
||||
expect(isProactivePaused()).toBe(isProactiveFeatureEnabled())
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user