chore: rebuild release package
This commit is contained in:
@@ -666,6 +666,50 @@ function assertFrontendSubagentCardMetadataContract() {
|
||||
mergeCollabAgentTaskState,
|
||||
};
|
||||
`)();
|
||||
const collabMergeStart = source.indexOf(' function toolKind(tool)');
|
||||
const collabMergeEnd = source.indexOf(' function collabStateLabel(statusText)', collabMergeStart);
|
||||
const childUpdateStart = source.indexOf(' function applyCcwebMcpChildAgentUpdate(msg)');
|
||||
const childUpdateEnd = source.indexOf(' function getDeleteConfirmMessage(agent)', childUpdateStart);
|
||||
assert(collabMergeStart >= 0 && collabMergeEnd > collabMergeStart, 'Frontend should expose collab merge helpers');
|
||||
assert(childUpdateStart >= 0 && childUpdateEnd > childUpdateStart, 'Frontend should define child-agent update handling before delete helpers');
|
||||
const collabApi = new Function(`
|
||||
let currentCwd = '';
|
||||
let currentSessionId = 'session-a';
|
||||
let closedCollabAgentIds = new Set();
|
||||
let collabAgentStateCache = new Map();
|
||||
let collabAgentIdsByToolUseId = new Map();
|
||||
let closedCollabAgentIdsByToolUseId = new Map();
|
||||
const activeToolCalls = new Map();
|
||||
let cachedSnapshot = {
|
||||
messages: [{
|
||||
toolCalls: [{
|
||||
id: 'tool-collab',
|
||||
kind: 'collab_agent_tool_call',
|
||||
input: {},
|
||||
}],
|
||||
}],
|
||||
};
|
||||
function updateCachedSession(sessionId, updater) {
|
||||
updater(cachedSnapshot);
|
||||
}
|
||||
function updateToolCall() {}
|
||||
function shortChildAgentId(id) {
|
||||
const value = String(id || '');
|
||||
return value.length > 12 ? value.slice(0, 8) : value;
|
||||
}
|
||||
${source.slice(collabMergeStart, collabMergeEnd)}
|
||||
${source.slice(childUpdateStart, childUpdateEnd)}
|
||||
return {
|
||||
mergeCollabAgentTools,
|
||||
applyCcwebMcpChildAgentUpdate,
|
||||
rememberCollabAgentState,
|
||||
getCachedState: (id) => collabAgentStateCache.get(id),
|
||||
hasCachedState: (id) => collabAgentStateCache.has(id),
|
||||
cacheSize: () => collabAgentStateCache.size,
|
||||
agentIdsForTool: (id) => new Set(collabAgentIdsByToolUseId.get(id) || []),
|
||||
closedIdsForTool: (id) => new Set(closedCollabAgentIdsByToolUseId.get(id) || []),
|
||||
};
|
||||
`)();
|
||||
|
||||
assert(source.includes('function pickCollabAgentTitle(state, id, index)'), 'Frontend should pick sub-agent titles through a dedicated helper');
|
||||
assert(
|
||||
@@ -747,6 +791,125 @@ function assertFrontendSubagentCardMetadataContract() {
|
||||
|
||||
const noPrompt = metadataApi.mergeCollabAgentTaskState({}, { label: '子代理' }, '', uuidV7, 0);
|
||||
assert(/^ID\s/.test(noPrompt.label) && noPrompt.label !== uuidV7, 'Missing prompts should fall back to a short thread id');
|
||||
|
||||
const spawnedTool = {
|
||||
id: 'tool-collab',
|
||||
name: 'spawn_agent',
|
||||
kind: 'collab_agent_tool_call',
|
||||
input: {
|
||||
tool: 'spawn_agent',
|
||||
prompt: '请实现子代理卡片关闭状态保留。',
|
||||
receiverThreadIds: ['child-thread-a'],
|
||||
agentsStates: {
|
||||
'child-thread-a': {
|
||||
title: '关闭验证代理',
|
||||
taskDescription: '请实现子代理卡片关闭状态保留。',
|
||||
role: 'implementer',
|
||||
status: 'running',
|
||||
},
|
||||
},
|
||||
},
|
||||
done: false,
|
||||
};
|
||||
const spawnedMerge = collabApi.mergeCollabAgentTools([spawnedTool]);
|
||||
assert(spawnedMerge.input.receiverThreadIds.length === 1, 'Spawned child should merge into one visible agent');
|
||||
assert(collabApi.getCachedState('child-thread-a')?.label === '关闭验证代理', 'Structured child state should be cached by thread id');
|
||||
|
||||
collabApi.rememberCollabAgentState(
|
||||
'unrelated-child',
|
||||
{ title: '无关历史代理', status: 'running' },
|
||||
'请处理无关历史任务。',
|
||||
1
|
||||
);
|
||||
collabApi.applyCcwebMcpChildAgentUpdate({
|
||||
type: 'ccweb_mcp_child_agent_update',
|
||||
sessionId: 'session-a',
|
||||
toolUseId: 'tool-collab',
|
||||
child: {
|
||||
threadId: 'child-thread-a',
|
||||
status: 'closed',
|
||||
},
|
||||
tool: {
|
||||
id: 'tool-collab',
|
||||
name: 'close_agent',
|
||||
kind: 'collab_agent_tool_call',
|
||||
input: { tool: 'close_agent', receiverThreadIds: [], agentsStates: {} },
|
||||
result: JSON.stringify({ status: 'closed', receiverThreadIds: [], agentsStates: {} }),
|
||||
done: true,
|
||||
},
|
||||
});
|
||||
const closedMerge = collabApi.mergeCollabAgentTools([
|
||||
{
|
||||
id: 'tool-collab',
|
||||
name: 'close_agent',
|
||||
kind: 'collab_agent_tool_call',
|
||||
input: { tool: 'close_agent', receiverThreadIds: [], agentsStates: {} },
|
||||
result: JSON.stringify({ status: 'closed', receiverThreadIds: [], agentsStates: {} }),
|
||||
done: true,
|
||||
},
|
||||
], {
|
||||
restoreAgentIds: new Set(['child-thread-a', 'unrelated-child']),
|
||||
closedAgentIds: collabApi.closedIdsForTool('tool-collab'),
|
||||
});
|
||||
assert(closedMerge.input.receiverThreadIds.length === 1, 'Empty close tool should keep exactly the closed child card');
|
||||
assert(closedMerge.input.receiverThreadIds[0] === 'child-thread-a', 'Empty close tool should restore only the closed child thread id');
|
||||
assert(!closedMerge.input.agentsStates['unrelated-child'], 'Empty close recovery must not mix unrelated cached children into the card');
|
||||
assert(closedMerge.input.agentsStates['child-thread-a'].label === '关闭验证代理', 'Empty close recovery should preserve the cached child title');
|
||||
assert(
|
||||
closedMerge.input.agentsStates['child-thread-a'].taskDescription === '请实现子代理卡片关闭状态保留。',
|
||||
'Empty close recovery should preserve the cached child introduction'
|
||||
);
|
||||
assert(closedMerge.input.agentsStates['child-thread-a'].status === 'closed', 'Empty close recovery should mark the child closed');
|
||||
|
||||
collabApi.rememberCollabAgentState(
|
||||
'wait-child-a',
|
||||
{ title: '等待验证代理', status: 'running' },
|
||||
'请等待子代理返回。',
|
||||
0
|
||||
);
|
||||
const emptyWaitMerge = collabApi.mergeCollabAgentTools([
|
||||
{
|
||||
id: 'tool-wait',
|
||||
name: 'wait_agent',
|
||||
kind: 'collab_agent_tool_call',
|
||||
input: { tool: 'wait_agent', receiverThreadIds: [], agentsStates: {} },
|
||||
result: JSON.stringify({ receiverThreadIds: [], agentsStates: {} }),
|
||||
done: false,
|
||||
},
|
||||
], {
|
||||
restoreAgentIds: new Set(['wait-child-a']),
|
||||
});
|
||||
assert(emptyWaitMerge.input.receiverThreadIds.length === 1, 'Empty wait tool should not reset a cached child card count to zero');
|
||||
assert(emptyWaitMerge.input.agentsStates['wait-child-a'].label === '等待验证代理', 'Empty wait recovery should preserve the cached child title');
|
||||
|
||||
const cacheSizeBeforeOrdinaryTool = collabApi.cacheSize();
|
||||
const ordinaryMerge = collabApi.mergeCollabAgentTools([
|
||||
{
|
||||
id: 'ordinary-tool',
|
||||
name: 'shell',
|
||||
kind: 'command_execution',
|
||||
input: {
|
||||
receiverThreadIds: ['ordinary-child'],
|
||||
agentsStates: {
|
||||
'ordinary-child': { title: '普通工具不应进入子代理缓存', status: 'closed' },
|
||||
},
|
||||
},
|
||||
done: true,
|
||||
},
|
||||
], {
|
||||
restoreAgentIds: new Set(['ordinary-child']),
|
||||
closedAgentIds: new Set(['ordinary-child']),
|
||||
});
|
||||
assert(ordinaryMerge === null, 'Non-collab tools should not enter the collab merge path');
|
||||
collabApi.applyCcwebMcpChildAgentUpdate({
|
||||
type: 'ccweb_mcp_child_agent_update',
|
||||
sessionId: 'session-a',
|
||||
toolUseId: 'ordinary-tool',
|
||||
child: { threadId: 'ordinary-child', status: 'closed' },
|
||||
tool: { id: 'ordinary-tool', name: 'shell', kind: 'command_execution', input: {}, result: '', done: true },
|
||||
});
|
||||
assert(collabApi.cacheSize() === cacheSizeBeforeOrdinaryTool, 'Non-collab child updates should not write sub-agent state cache');
|
||||
assert(!collabApi.hasCachedState('ordinary-child'), 'Non-collab tools should not cache child thread state');
|
||||
}
|
||||
|
||||
function assertFrontendPrimaryCodexAppUiContract() {
|
||||
@@ -914,6 +1077,35 @@ function assertSessionSwitchResilienceContract() {
|
||||
);
|
||||
}
|
||||
|
||||
function assertUnlimitedImageAttachmentsContract() {
|
||||
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
|
||||
|
||||
const handlerStart = frontendSource.indexOf('async function handleSelectedImageFiles(fileList)');
|
||||
const handlerEnd = frontendSource.indexOf('\n function getVisibleSessions()', handlerStart);
|
||||
assert(
|
||||
handlerStart >= 0 && handlerEnd > handlerStart,
|
||||
'Frontend should define handleSelectedImageFiles before getVisibleSessions'
|
||||
);
|
||||
const handlerBlock = frontendSource.slice(handlerStart, handlerEnd);
|
||||
assert(
|
||||
!/pendingAttachments\.length\s*\+\s*files\.length\s*>\s*\d+/.test(handlerBlock),
|
||||
'Frontend image attachment picker should not enforce a numeric per-message attachment cap'
|
||||
);
|
||||
assert(
|
||||
!/单条消息最多附带\s*\d+\s*张图片/.test(handlerBlock),
|
||||
'Frontend image attachment picker should not show a per-message image count limit'
|
||||
);
|
||||
assert(
|
||||
!serverSource.includes('MAX_MESSAGE_ATTACHMENTS'),
|
||||
'Server should not define or use MAX_MESSAGE_ATTACHMENTS'
|
||||
);
|
||||
assert(
|
||||
!/msg\.attachments\s*\.slice\s*\(\s*0\s*,/.test(serverSource),
|
||||
'Server should not truncate msg.attachments with slice'
|
||||
);
|
||||
}
|
||||
|
||||
function extractFunctionSource(source, name) {
|
||||
const start = source.indexOf(`function ${name}(`);
|
||||
assert(start >= 0, `Server should define ${name}`);
|
||||
@@ -945,6 +1137,94 @@ function extractFunctionSource(source, name) {
|
||||
throw new Error(`Could not parse function body for ${name}`);
|
||||
}
|
||||
|
||||
function assertCodexAppChildToolFallbackContract() {
|
||||
const source = fs.readFileSync(SERVER_PATH, 'utf8');
|
||||
const helperStart = source.indexOf('function parseMaybeJsonObject(value)');
|
||||
const helperEnd = source.indexOf('function sendCcwebMcpChildAgentUpdate(sessionId, child)', helperStart);
|
||||
assert(helperStart >= 0 && helperEnd > helperStart, 'Server should keep ccweb MCP child helper block before sendCcwebMcpChildAgentUpdate');
|
||||
const helperSource = source.slice(helperStart, helperEnd);
|
||||
const api = new Function(`
|
||||
const sessions = new Map();
|
||||
let savedSession = null;
|
||||
function truncateTextValue(value, maxLength, suffix = '...') {
|
||||
const text = String(value || '');
|
||||
return text.length > maxLength ? text.slice(0, maxLength - suffix.length) + suffix : text;
|
||||
}
|
||||
function loadSession(sessionId) {
|
||||
return sessions.get(sessionId) || null;
|
||||
}
|
||||
function saveSession(session) {
|
||||
savedSession = JSON.parse(JSON.stringify(session));
|
||||
sessions.set(session.id, session);
|
||||
}
|
||||
function findViewingSessionWs() {
|
||||
return null;
|
||||
}
|
||||
${helperSource}
|
||||
return {
|
||||
setSession: (session) => sessions.set(session.id, session),
|
||||
getSession: (sessionId) => sessions.get(sessionId),
|
||||
getSavedSession: () => savedSession,
|
||||
updatePersistedCcwebMcpChildTool,
|
||||
};
|
||||
`)();
|
||||
|
||||
const session = {
|
||||
id: 'fallback-session',
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
toolCalls: [
|
||||
{
|
||||
id: 'wait-collab-tool',
|
||||
name: 'wait_agent',
|
||||
kind: 'collab_agent_tool_call',
|
||||
input: { tool: 'wait_agent', receiverThreadIds: [], agentsStates: {} },
|
||||
result: JSON.stringify({ receiverThreadIds: [], agentsStates: {} }),
|
||||
done: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
toolCalls: [
|
||||
{
|
||||
id: 'ordinary-command-tool',
|
||||
name: 'shell',
|
||||
kind: 'command_execution',
|
||||
input: { command: 'echo should-not-be-selected' },
|
||||
result: 'ordinary output',
|
||||
done: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
api.setSession(session);
|
||||
const persistedTool = api.updatePersistedCcwebMcpChildTool(session.id, {
|
||||
threadId: 'child-refresh-thread',
|
||||
spawnToolId: 'missing-spawn-tool',
|
||||
label: '刷新路径代理',
|
||||
taskDescription: '验证刷新路径不会丢失子代理卡片。',
|
||||
status: 'closed',
|
||||
candidateResult: '子代理已关闭',
|
||||
closedAt: '2026-07-15T00:00:00.000Z',
|
||||
});
|
||||
assert(persistedTool?.id === 'wait-collab-tool', 'Missing spawnToolId should fall back to the latest collab tool');
|
||||
assert(session.messages[1].toolCalls[0].result === 'ordinary output', 'Fallback must not merge child state into ordinary command_execution tools');
|
||||
const result = JSON.parse(persistedTool.result);
|
||||
assert(result.receiverThreadIds.includes('child-refresh-thread'), 'Persisted fallback collab tool should include child thread id');
|
||||
assert(result.agentsStates?.['child-refresh-thread']?.title === '刷新路径代理', 'Persisted fallback collab tool should store child title');
|
||||
assert(
|
||||
result.agentsStates?.['child-refresh-thread']?.taskDescription === '验证刷新路径不会丢失子代理卡片。',
|
||||
'Persisted fallback collab tool should store child task description'
|
||||
);
|
||||
assert(result.agentsStates?.['child-refresh-thread']?.status === 'closed', 'Persisted fallback collab tool should store child closed status');
|
||||
assert(api.getSavedSession()?.id === session.id, 'Persisted fallback merge should save the session');
|
||||
}
|
||||
|
||||
function assertCodexAppUnroutedNotificationRoutingContract() {
|
||||
const source = fs.readFileSync(SERVER_PATH, 'utf8');
|
||||
assert(source.includes('const codexAppThreadSessionIndex = new Map();'), 'Server should keep an O(1) Codex App thread -> session index');
|
||||
@@ -996,6 +1276,7 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
assertUnlimitedImageAttachmentsContract();
|
||||
assertFrontendGenerationControlsContract();
|
||||
assertFrontendComposerMcpContract();
|
||||
assertFrontendSlashDraftPreservationContract();
|
||||
@@ -1007,6 +1288,7 @@ async function main() {
|
||||
assertFrontendPrimaryCodexAppUiContract();
|
||||
assertSetTitleMcpContract();
|
||||
assertSessionSwitchResilienceContract();
|
||||
assertCodexAppChildToolFallbackContract();
|
||||
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-regression-'));
|
||||
const configDir = path.join(tempRoot, 'config');
|
||||
@@ -2387,13 +2669,17 @@ async function main() {
|
||||
ws.send(JSON.stringify({ type: 'abort', sessionId: codexAppSession.sessionId }));
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||
|
||||
const claudeAttachment = await uploadAttachment(port, token, {
|
||||
filename: 'claude-test.png',
|
||||
const tinyPng = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=',
|
||||
'base64'
|
||||
);
|
||||
const claudeAttachments = await Promise.all(Array.from({ length: 5 }, (_, index) => uploadAttachment(port, token, {
|
||||
filename: `claude-test-${index + 1}.png`,
|
||||
mime: 'image/png',
|
||||
data: Buffer.from('claude-image'),
|
||||
});
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'describe attachment', attachments: [claudeAttachment], mode: 'yolo', agent: 'claude' }));
|
||||
const claudeImageSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'claude' && msg.title === 'describe attachment');
|
||||
data: tinyPng,
|
||||
})));
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'describe attachments', attachments: claudeAttachments, mode: 'yolo', agent: 'claude' }));
|
||||
const claudeImageSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'claude' && msg.title === 'describe attachments');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === claudeImageSession.sessionId);
|
||||
const claudeSpawnLine = fs.readFileSync(path.join(logsDir, 'process.log'), 'utf8')
|
||||
.trim()
|
||||
@@ -2401,7 +2687,15 @@ async function main() {
|
||||
.find((line) => line.includes(`"event":"process_spawn"`) && line.includes(claudeImageSession.sessionId.slice(0, 8)));
|
||||
assert(claudeSpawnLine && claudeSpawnLine.includes('--input-format stream-json'), 'Claude image message should switch stdin to stream-json');
|
||||
const storedClaudeSession = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${claudeImageSession.sessionId}.json`), 'utf8'));
|
||||
assert(Array.isArray(storedClaudeSession.messages?.[0]?.attachments) && storedClaudeSession.messages[0].attachments.length === 1, 'Claude message should persist attachment metadata');
|
||||
const storedClaudeUserMessage = storedClaudeSession.messages?.find((message) => message.role === 'user' && message.content === 'describe attachments');
|
||||
assert(
|
||||
Array.isArray(storedClaudeUserMessage?.attachments) && storedClaudeUserMessage.attachments.length === claudeAttachments.length,
|
||||
'Claude message should persist all attachment metadata'
|
||||
);
|
||||
const storedClaudeAttachmentNames = storedClaudeUserMessage.attachments.map((attachment) => attachment.filename);
|
||||
for (const attachment of claudeAttachments) {
|
||||
assert(storedClaudeAttachmentNames.includes(attachment.filename), `Claude message should preserve attachment ${attachment.filename}`);
|
||||
}
|
||||
assert(storedClaudeSession.claudeSessionId, 'Claude session id should be persisted after first run');
|
||||
const claudeSessionIdBeforeMode = storedClaudeSession.claudeSessionId;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user