chore: rebuild release package and commit updates
This commit is contained in:
@@ -647,6 +647,108 @@ function assertFrontendMcpReloadContract() {
|
||||
assert(source.includes('MCP 启动失败'), 'Frontend should expose a failed startup toast');
|
||||
}
|
||||
|
||||
function assertFrontendSubagentCardMetadataContract() {
|
||||
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
const styleSource = fs.readFileSync(PUBLIC_STYLE_PATH, 'utf8');
|
||||
|
||||
const metadataStart = source.indexOf(' function cleanCollabAgentText(value)');
|
||||
const metadataEnd = source.indexOf(' function normalizeCollabAgentAction(value)', metadataStart);
|
||||
assert(metadataStart >= 0 && metadataEnd > metadataStart, 'Frontend should expose an isolated sub-agent metadata helper block');
|
||||
const metadataApi = new Function(`
|
||||
function shortChildAgentId(id) {
|
||||
const value = String(id || '');
|
||||
return value.length > 12 ? value.slice(0, 8) : value;
|
||||
}
|
||||
${source.slice(metadataStart, metadataEnd)}
|
||||
return {
|
||||
isGenericCollabAgentLabel,
|
||||
pickCollabAgentTitle,
|
||||
mergeCollabAgentTaskState,
|
||||
};
|
||||
`)();
|
||||
|
||||
assert(source.includes('function pickCollabAgentTitle(state, id, index)'), 'Frontend should pick sub-agent titles through a dedicated helper');
|
||||
assert(
|
||||
/const titleCandidates = \[\s*state\.label,\s*state\.title,\s*state\.nickname,\s*state\.name,\s*\]/.test(source),
|
||||
'Sub-agent title priority should be label -> title -> nickname -> name'
|
||||
);
|
||||
assert(source.includes('isGenericCollabAgentLabel(candidate, id)'), 'Sub-agent title picker should skip generic labels and thread IDs');
|
||||
assert(source.includes('collabAgentTitleFromPrompt(taskDescription)'), 'Generic sub-agent titles should be derived from that agent prompt');
|
||||
assert(source.includes('return id ? `ID ${shortChildAgentId(id)}`'), 'Sub-agent title picker should fall back to a short thread id without a prompt');
|
||||
assert(source.includes('function mergeCollabAgentTaskState(previousState = {}, incomingState = {}'), 'Frontend should centralize per-agent task metadata merging');
|
||||
assert(source.includes('hasReadableSourceTitle: hasReadableCollabAgentTitle(state, id)'), 'Normalized child states should preserve whether a title came from protocol fields');
|
||||
assert(/mergeCollabAgentTaskState\(\s*states\[entry\.id\]/.test(source), 'Structured child states should use the tested metadata merge helper');
|
||||
assert(/mergeCollabAgentTaskState\(\s*states\[id\]/.test(source), 'Receiver-only child states should use the tested metadata merge helper');
|
||||
assert(source.includes('entry.detail ? `结果: ${entry.detail}` :'), 'Card title should keep runtime result in the container title');
|
||||
assert(source.includes("description.className = 'collab-agent-item-description'"), 'Sub-agent cards should render a visible task intro node');
|
||||
assert(source.includes('description.title = entry.taskDescription'), 'Task intro node should expose the full task intro in its title attribute');
|
||||
assert(source.includes('label.textContent = pickCollabAgentTitle(entry, entry.id, index);'), 'Rendered card label should use normalized title selection');
|
||||
assert(/\.collab-agent-item-description\s*\{[\s\S]*?-webkit-line-clamp:\s*2;/.test(styleSource), 'Sub-agent task intro should use two-line truncation');
|
||||
assert(/\.collab-agent-item\s*\{[\s\S]*?min-width:\s*0;[\s\S]*?flex-direction:\s*column;/.test(styleSource), 'Sub-agent cards should be vertically composed and flex-shrink on narrow screens');
|
||||
assert(/@media \(max-width:\s*640px\)[\s\S]*?\.collab-agent-item\s*\{[\s\S]*?min-width:\s*0;[\s\S]*?\}/.test(styleSource), 'Narrow screens should let sub-agent cards shrink without horizontal overflow');
|
||||
|
||||
const uuidV7 = '0190f01d-7b3e-7f03-9a5a-123456789abc';
|
||||
assert(metadataApi.isGenericCollabAgentLabel(uuidV7, 'different-thread-id'), 'UUID v7 labels should be treated as thread identifiers');
|
||||
assert(
|
||||
metadataApi.pickCollabAgentTitle({ label: '子代理', title: '架构审查', name: uuidV7 }, 'child-a', 0) === '架构审查',
|
||||
'Title selection should skip generic labels and preserve readable field priority'
|
||||
);
|
||||
|
||||
const firstSpawn = metadataApi.mergeCollabAgentTaskState(
|
||||
{},
|
||||
{ label: '子代理', status: 'running' },
|
||||
'请审查前端实现。核对标题和简介。',
|
||||
'child-thread-a',
|
||||
0
|
||||
);
|
||||
const secondSpawn = metadataApi.mergeCollabAgentTaskState(
|
||||
{},
|
||||
{ label: '子代理', status: 'running' },
|
||||
'请验证后端回归。核对状态同步。',
|
||||
'child-thread-b',
|
||||
1
|
||||
);
|
||||
assert(firstSpawn.taskDescription !== secondSpawn.taskDescription, 'Independent spawns should retain different task introductions');
|
||||
assert(firstSpawn.label !== secondSpawn.label, 'Independent spawns should derive different titles from their own prompts');
|
||||
|
||||
const afterWait = metadataApi.mergeCollabAgentTaskState(firstSpawn, { status: 'completed' }, '', 'child-thread-a', 0);
|
||||
const afterClose = metadataApi.mergeCollabAgentTaskState(afterWait, { status: 'closed' }, '', 'child-thread-a', 0);
|
||||
assert(afterWait.taskDescription === firstSpawn.taskDescription, 'Wait updates without a prompt should preserve the original task introduction');
|
||||
assert(afterClose.taskDescription === firstSpawn.taskDescription, 'Close updates without a prompt should preserve the original task introduction');
|
||||
|
||||
const namedSpawn = metadataApi.mergeCollabAgentTaskState({}, { name: '实现代理', status: 'running' }, '', 'child-thread-c', 2);
|
||||
const namedAfterWait = metadataApi.mergeCollabAgentTaskState(
|
||||
namedSpawn,
|
||||
{ label: 'ID child-th', title: '', nickname: '', name: '', status: 'completed' },
|
||||
'',
|
||||
'child-thread-c',
|
||||
2
|
||||
);
|
||||
assert(namedAfterWait.label === '实现代理', 'Status updates without a readable title should preserve the existing protocol title');
|
||||
|
||||
const namedAfterPromptDerivedUpdate = metadataApi.mergeCollabAgentTaskState(
|
||||
namedSpawn,
|
||||
{
|
||||
label: '整理前端改动并回报状态',
|
||||
name: 'child-thread-c',
|
||||
taskDescription: '请整理前端改动并回报状态。',
|
||||
hasReadableSourceTitle: false,
|
||||
status: 'completed',
|
||||
},
|
||||
'',
|
||||
'child-thread-c',
|
||||
2
|
||||
);
|
||||
assert(namedAfterPromptDerivedUpdate.label === '实现代理', 'Prompt-derived labels should not replace an existing protocol title');
|
||||
assert(
|
||||
namedAfterPromptDerivedUpdate.taskDescription === '请整理前端改动并回报状态。',
|
||||
'Prompt-derived updates may refresh the task introduction while preserving the protocol title'
|
||||
);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
function assertFrontendPrimaryCodexAppUiContract() {
|
||||
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
const indexSource = fs.readFileSync(PUBLIC_INDEX_PATH, 'utf8');
|
||||
@@ -812,7 +914,88 @@ function assertSessionSwitchResilienceContract() {
|
||||
);
|
||||
}
|
||||
|
||||
function extractFunctionSource(source, name) {
|
||||
const start = source.indexOf(`function ${name}(`);
|
||||
assert(start >= 0, `Server should define ${name}`);
|
||||
let parenDepth = 0;
|
||||
let signatureEnd = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') parenDepth += 1;
|
||||
if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(signatureEnd > start, `Server function ${name} should have a complete signature`);
|
||||
const open = source.indexOf('{', signatureEnd);
|
||||
assert(open > start, `Server function ${name} should have a body`);
|
||||
let depth = 0;
|
||||
for (let i = open; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) return source.slice(start, i + 1);
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not parse function body for ${name}`);
|
||||
}
|
||||
|
||||
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');
|
||||
assert(source.includes('const codexAppUnknownThreadMisses = new Map();'), 'Server should keep a bounded negative cache for unknown Codex App threads');
|
||||
assert(source.includes('const codexAppUnroutedNotificationLogTimes = new Map();'), 'Server should throttle unrouted notification logs by thread/method');
|
||||
assert(source.includes('function recoverCcwebMcpChildThreadsFromPersistedToolCalls'), 'Server should restore child-thread routes from persisted collaboration tool calls');
|
||||
assert(
|
||||
/recoverCcwebMcpChildThreadsFromPersistedToolCalls\(\s*sessionId,\s*state,\s*toolCalls\s*\)/.test(source),
|
||||
'Codex App recovery should rebuild persisted child-thread routes before cleaning run state'
|
||||
);
|
||||
assert(source.includes('function updateSessionRuntimeThreadIndex'), 'Server should centralize session thread index maintenance');
|
||||
assert(
|
||||
/function saveSession\(session\)[\s\S]*?updateSessionRuntimeThreadIndex\(session\)/.test(source),
|
||||
'Saving a session should refresh the runtime thread index'
|
||||
);
|
||||
assert(
|
||||
/function handleDeleteSession\(ws, sessionId\)[\s\S]*?removeSessionRuntimeThreadIndex\(sessionId\)/.test(source),
|
||||
'Deleting a session should remove its runtime thread index entry'
|
||||
);
|
||||
|
||||
const lookupBlock = extractFunctionSource(source, 'findCodexAppSessionByThreadId');
|
||||
assert(lookupBlock.includes('codexAppThreadSessionIndex.get(targetThreadId)'), 'Thread lookup should consult the O(1) index');
|
||||
assert(lookupBlock.includes('codexAppUnknownThreadMisses'), 'Thread lookup should use the unknown-thread negative cache');
|
||||
assert(!lookupBlock.includes('fs.readdirSync(SESSIONS_DIR)'), 'Thread lookup must not synchronously scan all session files on the notification hot path');
|
||||
|
||||
const routeBlock = extractFunctionSource(source, 'findCodexAppRouteByRuntime');
|
||||
const childIndex = routeBlock.indexOf("role: 'child'");
|
||||
const adoptIndex = routeBlock.indexOf('adoptCodexAppUnroutedTurn');
|
||||
assert(childIndex >= 0, 'Runtime routing should return child routes');
|
||||
assert(adoptIndex >= 0, 'Runtime routing should still adopt parent notifications from disk state');
|
||||
assert(childIndex < adoptIndex, 'Known child threads should route before parent disk adoption');
|
||||
|
||||
const notificationBlock = extractFunctionSource(source, 'handleCodexAppNotification');
|
||||
assert(
|
||||
notificationBlock.includes('shouldLogCodexAppUnroutedNotification(notification)'),
|
||||
'Unrouted notification logging should be throttled by a helper'
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const targetIndex = process.argv.indexOf('--target');
|
||||
const regressionTarget = targetIndex >= 0 ? String(process.argv[targetIndex + 1] || '').trim() : String(process.env.CC_WEB_REGRESSION_TARGET || '').trim();
|
||||
if (regressionTarget) {
|
||||
if (regressionTarget !== 'codexapp-unrouted-routing') {
|
||||
throw new Error(`Unknown regression target: ${regressionTarget}`);
|
||||
}
|
||||
assertCodexAppUnroutedNotificationRoutingContract();
|
||||
console.log('Codex App unrouted routing regression checks passed.');
|
||||
return;
|
||||
}
|
||||
|
||||
assertFrontendGenerationControlsContract();
|
||||
assertFrontendComposerMcpContract();
|
||||
assertFrontendSlashDraftPreservationContract();
|
||||
@@ -820,6 +1003,7 @@ async function main() {
|
||||
assertFrontendMarkdownLinkContract();
|
||||
assertMockCodexAppPromptUserNotTextTriggered();
|
||||
assertFrontendMcpReloadContract();
|
||||
assertFrontendSubagentCardMetadataContract();
|
||||
assertFrontendPrimaryCodexAppUiContract();
|
||||
assertSetTitleMcpContract();
|
||||
assertSessionSwitchResilienceContract();
|
||||
|
||||
Reference in New Issue
Block a user