chore: rebuild release package

This commit is contained in:
shiyue
2026-07-18 19:47:04 +08:00
parent b90f5e1e44
commit a038485ab5
9 changed files with 729 additions and 49 deletions

View File

@@ -538,6 +538,18 @@ function assertFrontendSidebarCollapseContract() {
assert(source.includes("const SIDEBAR_DRAWER_MEDIA_QUERY = '(max-width: 768px)'"), 'Sidebar drawer mode should match the mobile CSS breakpoint');
assert(source.includes("const SIDEBAR_DESKTOP_COLLAPSE_MEDIA_QUERY = '(width > 768px) and (hover: hover) and (pointer: fine)'"), 'Desktop collapse mode should match the hover rail CSS capabilities');
assert(source.includes("app.classList.toggle('sidebar-collapsed', nextCollapsed)"), 'Desktop sidebar should keep its fixed state on the app root');
assert(
indexSource.includes('id="user-outline-panel" class="user-outline-panel"')
&& indexSource.includes('id="ccweb-prompt-outline-panel" class="user-outline-panel ccweb-prompt-outline-panel"'),
'User-message and pending-form locators should share the responsive outline panel layout'
);
const baseOutlinePanelStyleStart = styleSource.indexOf('.user-outline-panel {');
const baseOutlinePanelStyleEnd = styleSource.indexOf('.user-outline-empty', baseOutlinePanelStyleStart);
const baseOutlinePanelStyle = styleSource.slice(baseOutlinePanelStyleStart, baseOutlinePanelStyleEnd);
assert(
/\.user-outline-panel\s*\{[^}]*position:\s*absolute;[^}]*right:\s*0;/.test(baseOutlinePanelStyle),
'Fixed-expanded desktop locators should retain their right-aligned base placement'
);
assert(source.includes('persistSidebarCollapsedPreference(nextCollapsed)'), 'Desktop sidebar toggle should persist the user preference');
assert(
source.includes("menuBtn.setAttribute('aria-expanded', String(expanded))")
@@ -555,10 +567,13 @@ function assertFrontendSidebarCollapseContract() {
);
assert(styleSource.includes('--sidebar-rail-width: 14px'), 'Collapsed desktop sidebar should retain a narrow hover rail');
const desktopCollapseStyleStart = styleSource.indexOf('@media (width > 768px) and (hover: hover) and (pointer: fine)');
const desktopCollapseStyleEnd = styleSource.indexOf('@media (prefers-reduced-motion: reduce)', desktopCollapseStyleStart);
assert(
styleSource.includes('@media (width > 768px) and (hover: hover) and (pointer: fine)'),
desktopCollapseStyleStart >= 0 && desktopCollapseStyleEnd > desktopCollapseStyleStart,
'Desktop hover behavior should stay isolated from touch and narrow-screen drawers'
);
const desktopCollapseStyle = styleSource.slice(desktopCollapseStyleStart, desktopCollapseStyleEnd);
assert(
styleSource.includes('margin-right: calc(var(--sidebar-rail-width) - var(--sidebar-width))')
&& styleSource.includes('transform: translateX(calc(var(--sidebar-rail-width) - var(--sidebar-width)))'),
@@ -569,6 +584,28 @@ function assertFrontendSidebarCollapseContract() {
&& styleSource.includes('transform: translateX(0)'),
'Collapsed sidebar should temporarily expand for pointer hover and keyboard focus'
);
assert(
/\.app\.sidebar-collapsed \.user-outline-panel\s*\{[^}]*left:\s*0;[^}]*right:\s*auto;[^}]*z-index:\s*120;/.test(desktopCollapseStyle),
'Both collapsed-sidebar locator panels should open toward the chat canvas above the hover preview'
);
const mobileSidebarStyleStart = styleSource.indexOf('@media (max-width: 768px)');
const mobileSidebarStyleEnd = styleSource.indexOf('@media (max-width: 480px)', mobileSidebarStyleStart);
const mobileSidebarStyle = styleSource.slice(mobileSidebarStyleStart, mobileSidebarStyleEnd);
assert(
/\.user-outline-panel\s*\{[^}]*left:\s*0;[^}]*right:\s*auto;[^}]*width:\s*min\(320px,\s*calc\(100vw - 20px\)\);/.test(mobileSidebarStyle),
'Closed mobile drawer should keep both locator panels inside the chat canvas'
);
const compactMobileStyleStart = mobileSidebarStyleEnd;
const compactMobileStyleEnd = styleSource.indexOf('/* === Utility === */', compactMobileStyleStart);
const compactMobileStyle = styleSource.slice(compactMobileStyleStart, compactMobileStyleEnd);
assert(
/\.user-outline-panel\s*\{[^}]*left:\s*auto;[^}]*right:\s*0;[^}]*width:\s*min\(320px,\s*calc\(100vw - 20px\)\);/.test(compactMobileStyle),
'Compact-mobile user locator should open left from the second grid column'
);
assert(
/\.ccweb-prompt-outline-panel\s*\{[^}]*left:\s*0;[^}]*right:\s*auto;/.test(compactMobileStyle),
'Compact-mobile pending-form locator should open right from the first grid column'
);
assert(styleSource.includes('.menu-btn:focus-visible'), 'Sidebar toggle should retain a visible keyboard focus treatment');
assert(
source.includes('if (isSidebarDesktopCollapseMode())')
@@ -581,8 +618,8 @@ function assertFrontendSidebarCollapseContract() {
'Rich themes should provide isolated rail treatments on top of the shared semantic fallback'
);
assert(
indexSource.includes('style.css?v=20260718-wasteland-bg-right-4')
&& indexSource.includes('app.js?v=20260718-wasteland-bg-right-4'),
indexSource.includes('style.css?v=20260718-shared-locator-panel-2')
&& indexSource.includes('app.js?v=20260718-shared-locator-panel-2'),
'Sidebar interaction assets should share the reviewed cache-busting version'
);
}
@@ -874,8 +911,8 @@ function assertFrontendGildedThemeContract() {
assert(contrast('#655446', '#fff7ea') >= 4.5, 'Gilded muted text should remain readable on ivory panels');
assert(contrast('#fff7ea', '#7a3f20') >= 7, 'Gilded primary action text should reach AAA contrast on copper');
assert(themeStyle.includes('@media (prefers-reduced-motion: reduce)'), 'Gilded theme motion should respect reduced-motion preferences');
assert(indexSource.includes('style.css?v=20260718-wasteland-bg-right-4'), 'Theme bundle stylesheet should use the current cache-busted asset URL');
assert(indexSource.includes('app.js?v=20260718-wasteland-bg-right-4'), 'Theme bundle app script should use the current cache-busted asset URL');
assert(indexSource.includes('style.css?v=20260718-shared-locator-panel-2'), 'Theme bundle stylesheet should use the current cache-busted asset URL');
assert(indexSource.includes('app.js?v=20260718-shared-locator-panel-2'), 'Theme bundle app script should use the current cache-busted asset URL');
}
function assertFrontendWastelandThemeContract() {
@@ -1128,8 +1165,8 @@ function assertFrontendWastelandThemeContract() {
assert(contrast('#c9bda6', backgroundColor) >= 4.5, `Wasteland muted text should reach AA contrast on ${backgroundColor}`);
});
assert(indexSource.includes('style.css?v=20260718-wasteland-bg-right-4'), 'Wasteland stylesheet should share the cache-busted theme bundle URL');
assert(indexSource.includes('app.js?v=20260718-wasteland-bg-right-4'), 'Wasteland registration should share the cache-busted theme bundle URL');
assert(indexSource.includes('style.css?v=20260718-shared-locator-panel-2'), 'Wasteland stylesheet should share the cache-busted theme bundle URL');
assert(indexSource.includes('app.js?v=20260718-shared-locator-panel-2'), 'Wasteland registration should share the cache-busted theme bundle URL');
}
function assertFrontendCcwebPromptContract() {
@@ -2130,6 +2167,472 @@ function assertSessionSwitchResilienceContract() {
);
}
function assertSessionRenderEpochRaceContract() {
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const beginSessionSwitchSource = extractFunctionSource(frontendSource, 'beginSessionSwitch');
const renderMessagesSource = extractFunctionSource(frontendSource, 'renderMessages');
const api = new Function(`
let renderEpoch = 0;
let loadedHistorySessionId = 'session-a';
let activeSessionLoad = null;
let currentSessionId = 'session-a';
let closedCollabAgentIds = new Set();
let collabAgentStateCache = new Map();
let collabAgentIdsByToolUseId = new Map();
let closedCollabAgentIdsByToolUseId = new Map();
const scheduled = [];
const messagesDiv = {
nodes: [],
scrollTop: 0,
get scrollHeight() { return this.nodes.length; },
get firstChild() { return this.nodes[0] || null; },
get innerHTML() { return ''; },
set innerHTML(value) { this.nodes = []; },
appendChild(node) {
this.nodes.push(...(node && node.__fragment ? node.nodes : [node]));
},
insertBefore(node) {
this.nodes.unshift(...(node && node.__fragment ? node.nodes : [node]));
},
};
const document = {
createDocumentFragment() {
return {
__fragment: true,
nodes: [],
appendChild(node) { this.nodes.push(node); },
};
},
};
const currentCwd = '/tmp/session-render-race';
function setTimeout(callback) { scheduled.push(callback); return scheduled.length; }
function setSessionLoading(sessionId) {
activeSessionLoad = sessionId ? { sessionId, overlayReleased: false } : null;
}
function requestSessionLoad() {}
function collectClosedCollabAgentIds() { return new Set(); }
function clearUserMessageIndex() {}
function buildWelcomeMarkup() { return '<p>welcome</p>'; }
function updateUserOutlinePanel() {}
function renderPendingNotes() {}
function scrollToBottom() {}
function updateScrollbar() {}
function buildMsgElement(message) { return { id: message.id }; }
${beginSessionSwitchSource}
${renderMessagesSource}
return {
beginSessionSwitch,
renderMessages,
flushTimers() {
while (scheduled.length > 0) scheduled.shift()();
},
renderedIds: () => messagesDiv.nodes.map((node) => node.id),
};
`)();
const sessionAMessages = Array.from({ length: 11 }, (_, index) => ({ id: `a-${index}` }));
api.renderMessages(sessionAMessages);
assert(
api.renderedIds().length === 10 && !api.renderedIds().includes('a-0'),
'Render epoch race fixture should leave the oldest A message in the delayed batch'
);
api.beginSessionSwitch('session-b', { force: true, blocking: false });
api.flushTimers();
assert(
JSON.stringify(api.renderedIds()) === JSON.stringify(sessionAMessages.map((message) => message.id)),
'Beginning a B load must not cancel A delayed batches before the B snapshot is committed'
);
api.renderMessages(sessionAMessages);
api.beginSessionSwitch('session-b', { force: true, blocking: false });
api.renderMessages([{ id: 'b-0' }]);
api.flushTimers();
assert(
JSON.stringify(api.renderedIds()) === JSON.stringify(['b-0']),
'Committing the B render must invalidate A delayed batches so stale messages cannot leak into B'
);
assert(
!/\brenderEpoch\s*(?:\+\+|--|[+\-*/%]?=)/.test(beginSessionSwitchSource),
'beginSessionSwitch should not advance renderEpoch before the replacement snapshot renders'
);
}
function assertSessionRequestIdRaceContract() {
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const sessionInfoStart = frontendSource.indexOf("case 'session_info':");
const sessionInfoDecisionStart = frontendSource.indexOf('const messageRequestId', sessionInfoStart);
const sessionInfoDecisionEnd = frontendSource.indexOf('if (!canSwitchToSessionInfo)', sessionInfoDecisionStart);
assert(
sessionInfoStart >= 0 && sessionInfoDecisionStart > sessionInfoStart && sessionInfoDecisionEnd > sessionInfoDecisionStart,
'Frontend should keep an extractable request gate before applying session_info'
);
const sessionInfoDecisionSource = frontendSource.slice(sessionInfoDecisionStart, sessionInfoDecisionEnd);
const decideSessionInfo = new Function('msg', 'snapshot', 'activeLoad', 'pendingNewSession', 'currentSessionId', `
${sessionInfoDecisionSource}
return { canSwitchToSessionInfo, matchesActiveLoad };
`);
const staleCurrentDecision = decideSessionInfo(
{
type: 'session_info',
sessionId: 'session-a',
requestId: 'request-old-a',
messages: [{ id: 'stale-a' }],
historyPending: false,
isRunning: false,
},
{
sessionId: 'session-a',
agent: 'codexapp',
messages: [{ id: 'stale-a' }],
},
{
sessionId: 'session-b',
requestId: 'request-new-b',
snapshot: null,
},
null,
'session-a'
);
const rejectsStaleCurrentSessionInfo = staleCurrentDecision.canSwitchToSessionInfo === false;
const finalizeLoadedSessionSource = extractFunctionSource(frontendSource, 'finalizeLoadedSession');
const finalizeApi = new Function(`
let activeSessionLoad = {
sessionId: 'session-b',
requestId: 'request-new-b',
snapshot: { messages: [{ id: 'b-0' }] },
};
const cached = [];
const finished = [];
function cacheSessionSnapshot(snapshot) { cached.push(snapshot); }
function finishSessionSwitch(sessionId) { finished.push(sessionId); }
${finalizeLoadedSessionSource}
return {
finalizeLoadedSession,
cached,
finished,
reset() { cached.length = 0; finished.length = 0; },
};
`)();
finalizeApi.finalizeLoadedSession('session-b', 'request-old-b');
const rejectsStaleFinalize = finalizeApi.cached.length === 0 && finalizeApi.finished.length === 0;
finalizeApi.reset();
finalizeApi.finalizeLoadedSession('session-b', 'request-new-b');
const acceptsMatchingFinalize = finalizeApi.cached.length === 1 && finalizeApi.finished.length === 1;
const historyStart = frontendSource.indexOf("case 'session_history_chunk':");
const historyEnd = frontendSource.indexOf("case 'session_message':", historyStart);
assert(historyStart >= 0 && historyEnd > historyStart, 'Frontend should keep an extractable session_history_chunk handler');
const historyCaseSource = frontendSource.slice(historyStart, historyEnd);
const historyApi = new Function(`
let activeSessionLoad = {
sessionId: 'session-b',
requestId: 'request-new-b',
recoverCurrent: false,
snapshot: { messages: [{ id: 'b-recent' }] },
};
let currentSessionId = 'session-b';
let loadedHistorySessionId = 'session-b';
const prepended = [];
const finalized = [];
function cloneMessages(messages) { return messages.slice(); }
function isBlockingSessionLoad() { return false; }
function prependHistoryMessages(messages) { prepended.push(...messages); }
function finalizeLoadedSession(sessionId, requestId) { finalized.push({ sessionId, requestId }); }
function handleHistoryMessage(msg) {
switch (msg.type) {
${historyCaseSource}
}
}
return {
handleHistoryMessage,
prepended,
finalized,
reset() { prepended.length = 0; finalized.length = 0; },
};
`)();
historyApi.handleHistoryMessage({
type: 'session_history_chunk',
sessionId: 'session-b',
requestId: 'request-old-b',
messages: [{ id: 'stale-history' }],
remaining: 0,
historyBaseIndex: 0,
});
const rejectsStaleHistoryChunk = historyApi.prepended.length === 0 && historyApi.finalized.length === 0;
historyApi.reset();
historyApi.handleHistoryMessage({
type: 'session_history_chunk',
sessionId: 'session-b',
requestId: 'request-new-b',
messages: [{ id: 'matching-history' }],
remaining: 0,
historyBaseIndex: 0,
});
const acceptsMatchingHistoryChunk = historyApi.prepended.some((message) => message.id === 'matching-history')
&& historyApi.finalized.some((entry) => entry.requestId === 'request-new-b');
const failures = [];
if (!rejectsStaleCurrentSessionInfo) {
failures.push('A stale requestId-bearing session_info for the current A view must not overwrite the pending B load');
}
if (!rejectsStaleFinalize) {
failures.push('finalizeLoadedSession must reject a stale requestId even when the sessionId still matches');
}
if (!acceptsMatchingFinalize) {
failures.push('finalizeLoadedSession should still commit the matching active load');
}
if (!rejectsStaleHistoryChunk) {
failures.push('session_history_chunk must reject stale requestIds before prepending or finalizing history');
}
if (!acceptsMatchingHistoryChunk) {
failures.push('session_history_chunk should preserve the matching active request path');
}
assert(failures.length === 0, failures.join('; '));
}
function assertBlockingFinishRafRequestRaceContract() {
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const isBlockingSessionLoadSource = extractFunctionSource(frontendSource, 'isBlockingSessionLoad');
const clearSessionLoadingSource = extractFunctionSource(frontendSource, 'clearSessionLoading');
const finishSessionSwitchSource = extractFunctionSource(frontendSource, 'finishSessionSwitch');
const api = new Function(`
let activeSessionLoad = {
sessionId: 'session-a',
requestId: 'request-a1',
blocking: true,
};
const rafCallbacks = [];
let scrollCount = 0;
function setSessionLoading(sessionId) {
activeSessionLoad = sessionId ? { sessionId, requestId: 'unexpected', blocking: true } : null;
}
function scrollToBottom() { scrollCount += 1; }
function requestAnimationFrame(callback) { rafCallbacks.push(callback); return rafCallbacks.length; }
${isBlockingSessionLoadSource}
${clearSessionLoadingSource}
${finishSessionSwitchSource}
return {
finishA1() { finishSessionSwitch('session-a', 'request-a1'); },
replaceWithA2() {
activeSessionLoad = {
sessionId: 'session-a',
requestId: 'request-a2',
blocking: true,
};
},
flushRaf() {
while (rafCallbacks.length > 0) rafCallbacks.shift()();
},
activeRequestId: () => activeSessionLoad?.requestId || null,
queuedRafCount: () => rafCallbacks.length,
scrollCount: () => scrollCount,
};
`)();
api.finishA1();
assert(
api.queuedRafCount() === 1 && api.scrollCount() === 1 && api.activeRequestId() === 'request-a1',
'Blocking A1 completion should defer clearing through the real finishSessionSwitch RAF path'
);
api.replaceWithA2();
api.flushRaf();
assert(
api.activeRequestId() === 'request-a2',
'The deferred A1 RAF callback must not clear a newer A2 load for the same sessionId'
);
}
function assertRecoverCurrentHistoryMergeContract() {
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const finalizeLoadedSessionSource = extractFunctionSource(frontendSource, 'finalizeLoadedSession');
const historyStart = frontendSource.indexOf("case 'session_history_chunk':");
const historyEnd = frontendSource.indexOf("case 'session_message':", historyStart);
assert(historyStart >= 0 && historyEnd > historyStart, 'Frontend should keep an extractable recovery history handler');
const historyCaseSource = frontendSource.slice(historyStart, historyEnd);
const api = new Function(`
let activeSessionLoad = {
sessionId: 'session-recover',
requestId: 'request-recover-new',
recoverCurrent: true,
blocking: false,
snapshot: { messages: [{ id: 'recent' }] },
};
let currentSessionId = 'session-recover';
let loadedHistorySessionId = 'session-recover';
const prepended = [];
const cached = [];
const finished = [];
function cloneMessages(messages) { return messages.map((message) => ({ ...message })); }
function isBlockingSessionLoad() { return false; }
function prependHistoryMessages(messages) { prepended.push(...messages); }
function cacheSessionSnapshot(snapshot) { cached.push(JSON.parse(JSON.stringify(snapshot))); }
function finishSessionSwitch(sessionId, requestId) { finished.push({ sessionId, requestId }); }
${finalizeLoadedSessionSource}
function handleHistoryMessage(msg) {
switch (msg.type) {
${historyCaseSource}
}
}
return {
handleHistoryMessage,
snapshotMessageIds: () => activeSessionLoad.snapshot.messages.map((message) => message.id),
prepended,
cached,
finished,
};
`)();
api.handleHistoryMessage({
type: 'session_history_chunk',
sessionId: 'session-recover',
requestId: 'request-recover-stale',
messages: [{ id: 'stale-history' }],
remaining: 0,
historyBaseIndex: 0,
});
const rejectsStaleChunk = JSON.stringify(api.snapshotMessageIds()) === JSON.stringify(['recent'])
&& api.prepended.length === 0
&& api.cached.length === 0
&& api.finished.length === 0;
api.handleHistoryMessage({
type: 'session_history_chunk',
sessionId: 'session-recover',
requestId: 'request-recover-new',
messages: [{ id: 'history-later' }],
remaining: 1,
historyBaseIndex: 1,
});
const mergesIntermediateChunk = JSON.stringify(api.snapshotMessageIds())
=== JSON.stringify(['history-later', 'recent']);
const keepsRecoveryOffDom = api.prepended.length === 0;
const defersRecoveryFinalize = api.cached.length === 0 && api.finished.length === 0;
api.handleHistoryMessage({
type: 'session_history_chunk',
sessionId: 'session-recover',
requestId: 'request-recover-new',
messages: [{ id: 'history-oldest' }],
remaining: 0,
historyBaseIndex: 0,
});
const cachedSnapshot = api.cached[0] || null;
const cachesCompleteHistory = api.cached.length === 1
&& cachedSnapshot.complete === true
&& JSON.stringify((cachedSnapshot.messages || []).map((message) => message.id))
=== JSON.stringify(['history-oldest', 'history-later', 'recent']);
const finalizesMatchingRecovery = api.finished.length === 1
&& api.finished[0].requestId === 'request-recover-new';
const neverPrependsRecoveryHistory = api.prepended.length === 0;
const failures = [];
if (!rejectsStaleChunk) failures.push('recoverCurrent must still reject stale requestId history chunks');
if (!mergesIntermediateChunk) failures.push('recoverCurrent must merge each accepted history chunk into the active snapshot');
if (!keepsRecoveryOffDom || !neverPrependsRecoveryHistory) failures.push('recoverCurrent history chunks must not prepend the live DOM');
if (!defersRecoveryFinalize) failures.push('recoverCurrent must wait for the last history chunk before caching');
if (!cachesCompleteHistory) failures.push('recoverCurrent finalization must cache recent and historical messages as one complete snapshot');
if (!finalizesMatchingRecovery) failures.push('recoverCurrent finalization must retain the matching requestId');
assert(failures.length === 0, failures.join('; '));
}
function assertServerSessionHistoryRequestIdContract() {
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
const handleLoadSessionSource = extractFunctionSource(serverSource, 'handleLoadSession');
const api = new Function(`
const sent = [];
const fixture = {
id: 'session-b',
title: 'Session B',
pinnedAt: null,
permissionMode: 'yolo',
model: 'gpt-5.5',
agent: 'codexapp',
hasUnread: false,
cwd: '/tmp/session-b',
totalCost: 0,
totalUsage: null,
updated: '2026-07-18T00:00:00.000Z',
messages: Array.from({ length: 11 }, (_, index) => ({ id: 'b-' + index })),
};
const activeProcesses = new Map();
const activeCodexAppTurns = new Map();
const wsSessionMap = new Map();
function sanitizeId(value) { return String(value || ''); }
function reconcilePendingCrossConversationReplies() {}
function loadSession() { return fixture; }
function wsSend(ws, message) { sent.push(message); }
function attachClientRequestId(message, source) {
return source && source.requestId ? { ...message, requestId: source.requestId } : message;
}
function flushPendingCrossConversationReplies() {}
function getSessionAgent(session) { return session.agent; }
function splitHistoryMessages(messages) {
return {
recentMessages: messages.slice(1),
olderChunks: [messages.slice(0, 1)],
historyRemaining: 0,
historyBuffered: messages.length,
};
}
function crossConversationWaitState() {
return {
waitingOnChildren: false,
pendingReplyCount: 0,
readyReplyCount: 0,
waitingReplyCount: 0,
failedReplyCount: 0,
pendingReplies: [],
};
}
function detachWsFromActiveRuntimes() {}
function saveSession() {}
function publicTitleMetadata() { return {}; }
function sessionModelLabel(session) { return session.model; }
function isSessionRunning() { return false; }
function attachActiveRuntimeToWs() {}
function resolveClaudeSessionLocalMeta() { return null; }
${handleLoadSessionSource}
return {
load(requestId) {
sent.length = 0;
handleLoadSession({}, { type: 'load_session', sessionId: fixture.id, requestId });
return sent.slice();
},
};
`)();
const messages = api.load('request-new-b');
const sessionInfo = messages.find((message) => message.type === 'session_info');
const historyChunks = messages.filter((message) => message.type === 'session_history_chunk');
assert(sessionInfo?.requestId === 'request-new-b', 'Server session_info fixture should echo the load requestId');
assert(historyChunks.length > 0, 'Server load fixture should emit at least one delayed history chunk');
assert(
historyChunks.every((message) => message.requestId === sessionInfo.requestId),
'Every server history chunk must echo the exact requestId carried by its session_info snapshot'
);
}
function assertSessionSwitchRaceContract() {
const checks = [
['render epoch behavior', assertSessionRenderEpochRaceContract],
['frontend requestId behavior', assertSessionRequestIdRaceContract],
['blocking finish RAF request behavior', assertBlockingFinishRafRequestRaceContract],
['recoverCurrent history merge behavior', assertRecoverCurrentHistoryMergeContract],
['server history requestId behavior', assertServerSessionHistoryRequestIdContract],
];
const failures = [];
for (const [label, check] of checks) {
try {
check();
} catch (err) {
failures.push(`${label}: ${err?.message || err}`);
}
}
if (failures.length > 0) {
throw new Error(`Session switch race regression failed:\n- ${failures.join('\n- ')}`);
}
}
function assertCodexAppStaleRunningRecoveryContract() {
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
const completeBlock = extractFunctionSource(serverSource, 'handleCodexAppTurnComplete');
@@ -2820,6 +3323,11 @@ async function main() {
console.log('Codex App stale running regression checks passed.');
return;
}
if (regressionTarget === 'session-switch-race') {
assertSessionSwitchRaceContract();
console.log('Session switch race regression checks passed.');
return;
}
throw new Error(`Unknown regression target: ${regressionTarget}`);
}
@@ -2839,6 +3347,7 @@ async function main() {
assertFrontendPrimaryCodexAppUiContract();
assertSetTitleMcpContract();
assertSessionSwitchResilienceContract();
assertSessionSwitchRaceContract();
assertCodexAppChildToolFallbackContract();
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-regression-'));