mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35:51 +00:00
* test: keep Codecov coverage on real agent communication paths PR #369 was merged before the final Codecov coverage fix landed, so this follow-up carries only the incremental real-path tests needed on top of main. The tests exercise AgentSummary lifecycle branches, mailbox fail-closed behavior, UDS client connection failure through a real capability file, and UDS response-reader framing without mock.module, warning suppression, feature fallback, or production-code churn. Constraint: PR #369 is already merged; this branch must contain only the incremental Codecov repair on top of latest main Rejected: Reopen or keep pushing the merged PR branch | merged PR refs do not update and would leave Codecov stale Rejected: Mock bun:bundle or hide warnings | would reintroduce cross-test pollution and pseudo coverage Rejected: Keep unrelated SendMessageTool production diff | it created avoidable patch-coverage debt without improving the runtime path Confidence: high Scope-risk: narrow Directive: Keep these coverage tests on real paths; do not replace them with output suppression or feature-flag mocks Tested: bunx tsc --noEmit --pretty false Tested: bun run lint Tested: bun test src\utils\__tests__\teammateMailbox.test.ts Tested: bun test src\services\AgentSummary\__tests__\agentSummary.test.ts src\services\AgentSummary\__tests__\summaryContext.test.ts src\utils\__tests__\teammateMailbox.test.ts src\utils\__tests__\udsMessaging.test.ts src\utils\__tests__\udsResponseReader.test.ts packages\builtin-tools\src\tools\SendMessageTool\__tests__\udsRecipientSanitization.test.ts Tested: bun run test:all Tested: bun test --coverage --coverage-reporter lcov --coverage-dir coverage Tested: bun run build Tested: bun run build:vite Tested: bun audit Tested: git diff --check Tested: Claude simplify review GO (.omx/artifacts/claude-simplify-codecov-20260427-1521.md) Tested: Claude security review GO (.omx/artifacts/claude-security-codecov-20260427-1522.md) Not-tested: GitHub-hosted Codecov upload after this amended commit until PR checks rerun * test: keep review assertions tied to real failure paths CodeRabbit flagged three non-blocking but valid review gaps: platform-specific mailbox errno checks, brittle UDS connection-failure message assertions, and missing AgentSummary reschedule proof after fork errors. This keeps the fixes narrow by tightening the affected assertions and adding a structured UDS connection error for tests to assert behavior instead of prose. Constraint: PR #374 is a review follow-up and must not hide warnings, skip tests, or merge the PR. Rejected: Matching the UDS failure message literal | preserves the brittle coupling CodeRabbit flagged. Rejected: Asserting only that mailbox writes throw | would allow unrelated pre-path failures to pass. Confidence: high Scope-risk: narrow Directive: Keep UDS connection-failure tests on structured error data, not display wording. Tested: bun test src/services/AgentSummary/__tests__/agentSummary.test.ts src/utils/__tests__/teammateMailbox.test.ts src/utils/__tests__/udsMessaging.test.ts Tested: bunx tsc --noEmit --pretty false Tested: bun run lint Tested: bun run test:all Tested: bun test --coverage --coverage-reporter lcov --coverage-dir coverage Tested: bun run build Tested: bun run build:vite Not-tested: GitHub-hosted CodeRabbit refresh until pushed. * test: remove brittle review follow-up assumptions CodeRabbit's second pass found two valid brittleness issues and one suggested callback-reference assertion that would not match production behavior. This keeps the production behavior unchanged: timers still schedule the summarizer closure, tests now assert timer-handle identity, and UDS connection errors use native Error.cause instead of shadowing it. Constraint: Do not manufacture behavior just to satisfy a review hint; assertions must match the real AgentSummary scheduling contract. Rejected: Assert a fresh scheduled callback function | scheduleNext intentionally passes the same runSummary closure each time. Rejected: Store a custom cause field on UdsPeerConnectionError | native Error.cause is available under ESNext/Bun. Confidence: high Scope-risk: narrow Directive: Timer tests should assert returned handle identity for ownership, not incidental numeric values. Tested: bun test src/services/AgentSummary/__tests__/agentSummary.test.ts src/utils/__tests__/udsMessaging.test.ts Tested: bunx tsc --noEmit --pretty false Tested: bun run lint Tested: bun run test:all Tested: bun test --coverage --coverage-reporter lcov --coverage-dir coverage Tested: bun run build Tested: bun run build:vite Not-tested: GitHub-hosted CodeRabbit refresh until pushed. * test: enforce structured UDS timeout failures CodeRabbit's follow-up surfaced a real consistency gap: UDS send socket errors used UdsPeerConnectionError while response timeouts still rejected a generic Error. Timeouts now use the same structured peer failure contract, and the test exercises that path through a short explicit timeout instead of waiting for the production default. The AgentSummary unchanged-fingerprint test now also asserts that the second unchanged tick does not log errors, preserving the existing behavior checks without changing production scheduling semantics. Constraint: Keep the production timeout default at 5000ms while allowing tests to exercise the timeout path quickly. Rejected: Leave timeout failures as generic Error | callers would need separate handling for the same peer connection failure class. Confidence: high Scope-risk: narrow Directive: Keep UDS send timeout and socket-error branches on the same structured error contract. Tested: bun test src/services/AgentSummary/__tests__/agentSummary.test.ts src/utils/__tests__/udsMessaging.test.ts Tested: bunx tsc --noEmit --pretty false Tested: bun run lint Tested: bun run test:all Tested: bun test --coverage --coverage-reporter lcov --coverage-dir coverage Tested: bun run build Tested: bun run build:vite Not-tested: GitHub-hosted CodeRabbit refresh until pushed. --------- Co-authored-by: unraid <local@unraid.local>
501 lines
14 KiB
TypeScript
501 lines
14 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
|
import { mkdtempSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join } from 'node:path'
|
|
import type { Message } from 'src/types/message.js'
|
|
import { getErrnoCode } from 'src/utils/errors.js'
|
|
import {
|
|
compactMailboxMessages,
|
|
getLastPeerDmSummary,
|
|
getInboxPath,
|
|
markMessageAsReadByIndex,
|
|
markMessageAsReadByIdentity,
|
|
markMessagesAsRead,
|
|
markMessagesAsReadByPredicate,
|
|
MAX_MAILBOX_MESSAGE_TEXT_BYTES,
|
|
MAX_MAILBOX_FILE_BYTES,
|
|
MAX_MAILBOX_MESSAGES,
|
|
MAX_READ_MAILBOX_MESSAGES,
|
|
MAX_UNREAD_PROTOCOL_MAILBOX_MESSAGES,
|
|
readMailbox,
|
|
type TeammateMessage,
|
|
writeToMailbox,
|
|
} from 'src/utils/teammateMailbox.js'
|
|
|
|
let tempHome = ''
|
|
let previousConfigDir: string | undefined
|
|
|
|
function message(
|
|
text: string,
|
|
read: boolean,
|
|
timestamp = new Date(0).toISOString(),
|
|
): TeammateMessage {
|
|
return {
|
|
from: 'team-lead',
|
|
text,
|
|
timestamp,
|
|
read,
|
|
}
|
|
}
|
|
|
|
async function seedMailbox(
|
|
agentName: string,
|
|
teamName: string,
|
|
messages: TeammateMessage[],
|
|
): Promise<void> {
|
|
const inboxPath = getInboxPath(agentName, teamName)
|
|
await mkdir(dirname(inboxPath), { recursive: true })
|
|
await writeFile(inboxPath, JSON.stringify(messages, null, 2), 'utf-8')
|
|
}
|
|
|
|
async function readRawMailbox(
|
|
agentName: string,
|
|
teamName: string,
|
|
): Promise<TeammateMessage[]> {
|
|
const content = await readFile(getInboxPath(agentName, teamName), 'utf-8')
|
|
return JSON.parse(content) as TeammateMessage[]
|
|
}
|
|
|
|
describe('compactMailboxMessages', () => {
|
|
test('prioritizes unread messages and keeps only recent read history', () => {
|
|
const compacted = compactMailboxMessages(
|
|
[
|
|
message('read-1', true),
|
|
message('read-2', true),
|
|
message('unread-1', false),
|
|
message('read-3', true),
|
|
message('unread-2', false),
|
|
message('read-4', true),
|
|
message('read-5', true),
|
|
message('unread-3', false),
|
|
],
|
|
{ maxMessages: 5, maxReadMessages: 2 },
|
|
)
|
|
|
|
expect(compacted.map(m => m.text)).toEqual([
|
|
'unread-1',
|
|
'unread-2',
|
|
'read-4',
|
|
'read-5',
|
|
'unread-3',
|
|
])
|
|
})
|
|
|
|
test('retains unread protocol messages separately from regular cap', () => {
|
|
const protocol = message(
|
|
JSON.stringify({ type: 'permission_response', request_id: 'req-1' }),
|
|
false,
|
|
)
|
|
const compacted = compactMailboxMessages(
|
|
[
|
|
protocol,
|
|
...Array.from({ length: 5 }, (_value, index) =>
|
|
message(`regular-${index}`, false),
|
|
),
|
|
],
|
|
{
|
|
maxMessages: 2,
|
|
maxReadMessages: 0,
|
|
maxUnreadProtocolMessages: 1,
|
|
},
|
|
)
|
|
|
|
expect(compacted.map(m => m.text)).toEqual([
|
|
protocol.text,
|
|
'regular-3',
|
|
'regular-4',
|
|
])
|
|
})
|
|
|
|
test('does not prioritize malformed JSON-like unread messages as protocol', () => {
|
|
const compacted = compactMailboxMessages(
|
|
[
|
|
message('{not-json', false),
|
|
message('regular-1', false),
|
|
message('regular-2', false),
|
|
],
|
|
{
|
|
maxMessages: 1,
|
|
maxReadMessages: 0,
|
|
maxUnreadProtocolMessages: 10,
|
|
},
|
|
)
|
|
|
|
expect(compacted.map(m => m.text)).toEqual(['regular-2'])
|
|
})
|
|
|
|
test('caps unread protocol messages with an independent bound', () => {
|
|
const compacted = compactMailboxMessages(
|
|
Array.from(
|
|
{ length: MAX_UNREAD_PROTOCOL_MAILBOX_MESSAGES + 1 },
|
|
(_value, index) =>
|
|
message(
|
|
JSON.stringify({
|
|
type: 'permission_response',
|
|
request_id: `req-${index}`,
|
|
}),
|
|
false,
|
|
),
|
|
),
|
|
)
|
|
|
|
expect(compacted).toHaveLength(MAX_UNREAD_PROTOCOL_MAILBOX_MESSAGES)
|
|
expect(compacted[0]?.text).toContain('req-1')
|
|
})
|
|
|
|
test('keeps retained mailbox bytes under an explicit budget', () => {
|
|
const compacted = compactMailboxMessages(
|
|
Array.from({ length: 20 }, (_value, index) =>
|
|
message(`msg-${index}-${'x'.repeat(200)}`, false),
|
|
),
|
|
{
|
|
maxMessages: 20,
|
|
maxReadMessages: 0,
|
|
maxRetainedBytes: 1_000,
|
|
},
|
|
)
|
|
|
|
expect(
|
|
Buffer.byteLength(JSON.stringify(compacted), 'utf8'),
|
|
).toBeLessThanOrEqual(1_000)
|
|
expect(compacted.length).toBeLessThan(20)
|
|
expect(compacted.at(-1)?.text).toContain('msg-19')
|
|
})
|
|
|
|
test('returns an empty mailbox when even one message exceeds retained budget', () => {
|
|
const compacted = compactMailboxMessages([message('too-large', false)], {
|
|
maxMessages: 10,
|
|
maxReadMessages: 0,
|
|
maxRetainedBytes: 1,
|
|
})
|
|
|
|
expect(compacted).toEqual([])
|
|
})
|
|
|
|
test('returns an empty mailbox when all retention lanes are disabled', () => {
|
|
const compacted = compactMailboxMessages([message('unread', false)], {
|
|
maxMessages: 0,
|
|
maxReadMessages: 0,
|
|
maxUnreadProtocolMessages: 0,
|
|
maxRetainedBytes: 1_000,
|
|
})
|
|
|
|
expect(compacted).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('teammate mailbox retention', () => {
|
|
beforeEach(() => {
|
|
previousConfigDir = process.env.CLAUDE_CONFIG_DIR
|
|
tempHome = mkdtempSync(join(tmpdir(), 'teammate-mailbox-'))
|
|
process.env.CLAUDE_CONFIG_DIR = tempHome
|
|
})
|
|
|
|
afterEach(async () => {
|
|
if (previousConfigDir === undefined) {
|
|
delete process.env.CLAUDE_CONFIG_DIR
|
|
} else {
|
|
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
|
|
}
|
|
await rm(tempHome, { recursive: true, force: true })
|
|
tempHome = ''
|
|
})
|
|
|
|
test('writeToMailbox compacts oversized unread inbox files', async () => {
|
|
const existing = Array.from(
|
|
{ length: MAX_MAILBOX_MESSAGES + 20 },
|
|
(_value, index) => message(`old-${index}`, false),
|
|
)
|
|
await seedMailbox('worker', 'alpha', existing)
|
|
|
|
await writeToMailbox(
|
|
'worker',
|
|
{
|
|
from: 'team-lead',
|
|
text: 'newest',
|
|
timestamp: new Date(1).toISOString(),
|
|
},
|
|
'alpha',
|
|
)
|
|
|
|
const after = await readMailbox('worker', 'alpha')
|
|
expect(after).toHaveLength(MAX_MAILBOX_MESSAGES)
|
|
expect(after[0]?.text).toBe('old-21')
|
|
expect(after.at(-1)?.text).toBe('newest')
|
|
})
|
|
|
|
test('markMessagesAsRead compacts read history after consumption', async () => {
|
|
const existing = Array.from(
|
|
{ length: MAX_MAILBOX_MESSAGES + 20 },
|
|
(_value, index) => message(`msg-${index}`, false),
|
|
)
|
|
await seedMailbox('worker', 'alpha', existing)
|
|
|
|
await markMessagesAsRead('worker', 'alpha')
|
|
|
|
const after = await readRawMailbox('worker', 'alpha')
|
|
expect(after).toHaveLength(MAX_READ_MAILBOX_MESSAGES)
|
|
expect(after.every(m => m.read)).toBe(true)
|
|
expect(after[0]?.text).toBe(
|
|
`msg-${MAX_MAILBOX_MESSAGES + 20 - MAX_READ_MAILBOX_MESSAGES}`,
|
|
)
|
|
})
|
|
|
|
test('markMessagesAsReadByPredicate leaves structured messages unread', async () => {
|
|
await seedMailbox('worker', 'alpha', [
|
|
message('plain', false),
|
|
message(JSON.stringify({ type: 'permission_request' }), false),
|
|
])
|
|
|
|
await markMessagesAsReadByPredicate(
|
|
'worker',
|
|
m => !m.text.includes('permission_request'),
|
|
'alpha',
|
|
)
|
|
|
|
const after = await readRawMailbox('worker', 'alpha')
|
|
expect(after.map(m => m.read)).toEqual([true, false])
|
|
})
|
|
|
|
test('markMessageAsReadByIdentity survives compaction shifting indexes', async () => {
|
|
const permissionResponse = message(
|
|
JSON.stringify({ type: 'permission_response', request_id: 'req-1' }),
|
|
false,
|
|
)
|
|
await seedMailbox('worker', 'alpha', [
|
|
permissionResponse,
|
|
...Array.from({ length: MAX_MAILBOX_MESSAGES + 20 }, (_value, index) =>
|
|
message(`regular-${index}`, false),
|
|
),
|
|
])
|
|
|
|
await writeToMailbox(
|
|
'worker',
|
|
{
|
|
from: 'team-lead',
|
|
text: 'newest',
|
|
timestamp: new Date(2).toISOString(),
|
|
},
|
|
'alpha',
|
|
)
|
|
const marked = await markMessageAsReadByIdentity(
|
|
'worker',
|
|
'alpha',
|
|
permissionResponse,
|
|
)
|
|
|
|
const after = await readRawMailbox('worker', 'alpha')
|
|
expect(marked).toBe(true)
|
|
expect(after.some(m => m.text === permissionResponse.text && !m.read)).toBe(
|
|
false,
|
|
)
|
|
})
|
|
|
|
test('markMessageAsReadByIndex also compacts through the compatibility path', async () => {
|
|
const existing = Array.from(
|
|
{ length: MAX_MAILBOX_MESSAGES + 10 },
|
|
(_value, index) => message(`msg-${index}`, false),
|
|
)
|
|
await seedMailbox('worker', 'alpha', existing)
|
|
|
|
await markMessageAsReadByIndex('worker', 'alpha', existing.length - 1)
|
|
|
|
const after = await readRawMailbox('worker', 'alpha')
|
|
expect(after).toHaveLength(MAX_MAILBOX_MESSAGES)
|
|
expect(after.some(m => m.text === `msg-${existing.length - 1}`)).toBe(false)
|
|
expect(after.at(-1)?.text).toBe(`msg-${existing.length - 2}`)
|
|
})
|
|
|
|
test('writeToMailbox rejects oversized message text instead of storing it', async () => {
|
|
await expect(
|
|
writeToMailbox(
|
|
'worker',
|
|
{
|
|
from: 'team-lead',
|
|
text: 'x'.repeat(MAX_MAILBOX_MESSAGE_TEXT_BYTES + 1),
|
|
timestamp: new Date(3).toISOString(),
|
|
},
|
|
'alpha',
|
|
),
|
|
).rejects.toThrow('Mailbox message text exceeds')
|
|
|
|
expect(await readRawMailbox('worker', 'alpha')).toEqual([])
|
|
})
|
|
|
|
test('writeToMailbox fails closed when an existing mailbox is corrupt', async () => {
|
|
const inboxPath = getInboxPath('worker', 'alpha')
|
|
await mkdir(dirname(inboxPath), { recursive: true })
|
|
await writeFile(inboxPath, '{not-json', 'utf-8')
|
|
|
|
await expect(
|
|
writeToMailbox(
|
|
'worker',
|
|
{
|
|
from: 'team-lead',
|
|
text: 'new',
|
|
timestamp: new Date(4).toISOString(),
|
|
},
|
|
'alpha',
|
|
),
|
|
).rejects.toThrow()
|
|
|
|
expect(await readFile(inboxPath, 'utf-8')).toBe('{not-json')
|
|
})
|
|
|
|
test('writeToMailbox rejects when the inbox path is already a directory', async () => {
|
|
const inboxPath = getInboxPath('worker', 'alpha')
|
|
await mkdir(inboxPath, { recursive: true })
|
|
|
|
const error = await writeToMailbox(
|
|
'worker',
|
|
{
|
|
from: 'team-lead',
|
|
text: 'new',
|
|
timestamp: new Date(5).toISOString(),
|
|
},
|
|
'alpha',
|
|
).then(
|
|
() => undefined,
|
|
err => err,
|
|
)
|
|
|
|
const code = getErrnoCode(error)
|
|
expect(code).toBeDefined()
|
|
if (code === undefined) {
|
|
throw new Error('Expected filesystem errno code')
|
|
}
|
|
expect(['EISDIR', 'EPERM', 'EACCES']).toContain(code)
|
|
expect((await stat(inboxPath)).isDirectory()).toBe(true)
|
|
})
|
|
|
|
test('readMailbox fails closed on corrupt mailbox content', async () => {
|
|
const inboxPath = getInboxPath('worker', 'alpha')
|
|
await mkdir(dirname(inboxPath), { recursive: true })
|
|
await writeFile(inboxPath, '{not-json', 'utf-8')
|
|
|
|
await expect(readMailbox('worker', 'alpha')).rejects.toThrow()
|
|
})
|
|
|
|
test('readMailbox rejects non-array mailbox files', async () => {
|
|
const inboxPath = getInboxPath('worker', 'alpha')
|
|
await mkdir(dirname(inboxPath), { recursive: true })
|
|
await writeFile(inboxPath, JSON.stringify({ text: 'not an array' }), 'utf-8')
|
|
|
|
await expect(readMailbox('worker', 'alpha')).rejects.toThrow(
|
|
'expected message array',
|
|
)
|
|
})
|
|
|
|
test('readMailbox rejects malformed stored message shapes', async () => {
|
|
const inboxPath = getInboxPath('worker', 'alpha')
|
|
await mkdir(dirname(inboxPath), { recursive: true })
|
|
await writeFile(
|
|
inboxPath,
|
|
JSON.stringify([{ from: 'lead', text: 'missing timestamp' }]),
|
|
'utf-8',
|
|
)
|
|
|
|
await expect(readMailbox('worker', 'alpha')).rejects.toThrow(
|
|
'Invalid mailbox message shape',
|
|
)
|
|
})
|
|
|
|
test('readMailbox rejects non-object stored messages', async () => {
|
|
const inboxPath = getInboxPath('worker', 'alpha')
|
|
await mkdir(dirname(inboxPath), { recursive: true })
|
|
await writeFile(inboxPath, JSON.stringify(['not an object']), 'utf-8')
|
|
|
|
await expect(readMailbox('worker', 'alpha')).rejects.toThrow(
|
|
'expected object',
|
|
)
|
|
})
|
|
|
|
test('readMailbox rejects oversized mailbox files before parsing', async () => {
|
|
const inboxPath = getInboxPath('worker', 'alpha')
|
|
await mkdir(dirname(inboxPath), { recursive: true })
|
|
await writeFile(inboxPath, `[${' '.repeat(MAX_MAILBOX_FILE_BYTES)}]`, 'utf-8')
|
|
|
|
await expect(readMailbox('worker', 'alpha')).rejects.toThrow(
|
|
'Mailbox file exceeds',
|
|
)
|
|
})
|
|
|
|
test('markMessageAsReadByIdentity returns false for missing mailbox files', async () => {
|
|
await expect(
|
|
markMessageAsReadByIdentity('worker', 'alpha', message('absent', false)),
|
|
).resolves.toBe(false)
|
|
})
|
|
|
|
test('markMessageAsReadByIdentity returns false when the expected message moved out', async () => {
|
|
await seedMailbox('worker', 'alpha', [message('other', false)])
|
|
|
|
await expect(
|
|
markMessageAsReadByIdentity('worker', 'alpha', message('missing', false)),
|
|
).resolves.toBe(false)
|
|
|
|
expect((await readRawMailbox('worker', 'alpha'))[0]?.read).toBe(false)
|
|
})
|
|
|
|
test('markMessageAsReadByIdentity returns false on corrupt mailbox content', async () => {
|
|
const inboxPath = getInboxPath('worker', 'alpha')
|
|
await mkdir(dirname(inboxPath), { recursive: true })
|
|
await writeFile(inboxPath, '{not-json', 'utf-8')
|
|
|
|
await expect(
|
|
markMessageAsReadByIdentity('worker', 'alpha', message('missing', false)),
|
|
).resolves.toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('getLastPeerDmSummary', () => {
|
|
test('extracts the final peer direct-message summary from assistant tool use', () => {
|
|
const messages = [
|
|
{ type: 'user', message: { content: 'wake up' } },
|
|
{
|
|
type: 'assistant',
|
|
message: {
|
|
content: [
|
|
{
|
|
type: 'tool_use',
|
|
name: 'SendMessage',
|
|
input: {
|
|
to: 'worker-1',
|
|
message: 'please check the UDS bounds',
|
|
summary: 'Checking UDS bounds',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
] as unknown as Message[]
|
|
|
|
expect(getLastPeerDmSummary(messages)).toBe(
|
|
'[to worker-1] Checking UDS bounds',
|
|
)
|
|
})
|
|
|
|
test('stops peer direct-message summary search at the wake-up boundary', () => {
|
|
const messages = [
|
|
{
|
|
type: 'assistant',
|
|
message: {
|
|
content: [
|
|
{
|
|
type: 'tool_use',
|
|
name: 'SendMessage',
|
|
input: {
|
|
to: 'worker-1',
|
|
message: 'old message',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{ type: 'user', message: { content: 'new prompt' } },
|
|
] as unknown as Message[]
|
|
|
|
expect(getLastPeerDmSummary(messages)).toBeUndefined()
|
|
})
|
|
})
|