修复完整会话历史与长气泡交互并更新 CentOS 7 发布包
提交当前工作区全部源码、回归脚本、任务记录及重新构建的 Bun baseline 发布包。发布单文件 MCP 初始化及前端资源一致性验证通过。
This commit is contained in:
@@ -48,9 +48,13 @@ async function waitFor(predicate, description, timeout = 12000) {
|
||||
}
|
||||
async function stopChild(child) {
|
||||
if (!child || child.exitCode !== null || child.signalCode) return;
|
||||
const exited = new Promise((resolve) => child.once('exit', resolve));
|
||||
child.kill('SIGTERM');
|
||||
await Promise.race([new Promise((resolve) => child.once('exit', resolve)), sleep(1000)]);
|
||||
if (child.exitCode === null && !child.signalCode) child.kill('SIGKILL');
|
||||
await Promise.race([exited, sleep(1000)]);
|
||||
if (child.exitCode === null && !child.signalCode) {
|
||||
child.kill('SIGKILL');
|
||||
await exited;
|
||||
}
|
||||
}
|
||||
|
||||
class FirefoxDriver {
|
||||
@@ -168,7 +172,11 @@ Module._extensions['.js'] = (module, filename) => {
|
||||
}, 80);
|
||||
`).replace("path.join(__dirname, 'mock-codex.js')", JSON.stringify(path.join(REPO, 'scripts/mock-codex.js')));
|
||||
fs.writeFileSync(mock, browserMock, { mode: 0o755 });
|
||||
return { home, sessions, rollout, preload, mock, completeFlag, snapshotCount: snapshot.length, nativeCount: store.parseCodexRolloutLines(rows.map((row) => JSON.stringify(row))).messages.length };
|
||||
return {
|
||||
home, sessions, rollout, preload, mock, completeFlag, snapshotCount: snapshot.length,
|
||||
historyIds: [...prefix, ...snapshot.slice(-4)].map((message) => message.id),
|
||||
nativeCount: store.parseCodexRolloutLines(rows.map((row) => JSON.stringify(row))).messages.length,
|
||||
};
|
||||
}
|
||||
|
||||
async function startRelay(port, targetPort, evidence) {
|
||||
@@ -192,8 +200,13 @@ async function startRelay(port, targetPort, evidence) {
|
||||
try {
|
||||
const msg = JSON.parse(String(data));
|
||||
if (['session_info', 'resume_session_result', 'session_history_chunk'].includes(msg.type)) evidence.protocol.push({
|
||||
type: msg.type, historySource: msg.historySource, historyTotal: msg.historyTotal, isRunning: msg.isRunning,
|
||||
type: msg.type, historySource: msg.historySource, historyTotal: msg.historyTotal,
|
||||
historyAvailable: msg.historyAvailable, isRunning: msg.isRunning,
|
||||
ids: msg.messages?.map((message) => message.id),
|
||||
messages: msg.messages?.map((message, index) => ({
|
||||
id: message.id,
|
||||
index: Number.isFinite(Number(msg.historyBaseIndex)) ? Number(msg.historyBaseIndex) + index : null,
|
||||
})),
|
||||
});
|
||||
} catch {}
|
||||
if (client.readyState === WebSocket.OPEN) client.send(data, { binary: false });
|
||||
@@ -265,16 +278,23 @@ async function main() {
|
||||
const evidence = { root, fixture, protocol: [], stages: {}, kind: '真实 server.js / Firefox / 隔离持久化与 native rollout / mock Codex App' };
|
||||
const [serverPort, relayPort, driverPort] = await Promise.all([freePort(), freePort(), freePort()]);
|
||||
let server, gecko, relay, driver;
|
||||
let rolloutHidden = false;
|
||||
try {
|
||||
server = spawn(process.execPath, ['-r', fixture.preload, path.join(REPO, 'server.js')], {
|
||||
cwd: REPO, env: { ...process.env, PORT: String(serverPort), CC_WEB_PASSWORD: PASSWORD,
|
||||
CC_WEB_INTERNAL_MCP_TOKEN: MCP_TOKEN,
|
||||
CC_WEB_CONFIG_DIR: path.join(root, 'config'), CC_WEB_SESSIONS_DIR: fixture.sessions, CC_WEB_LOGS_DIR: path.join(root, 'logs'),
|
||||
CODEX_HOME: path.join(fixture.home, '.codex'),
|
||||
CODEX_PATH: fixture.mock, CLAUDE_PATH: path.join(REPO, 'scripts/mock-claude.js'), CC_WEB_TASK_BOARD_ENABLED: '0',
|
||||
}, stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
const serverLog = fs.createWriteStream(path.join(root, 'server.log')); server.stdout.pipe(serverLog); server.stderr.pipe(serverLog);
|
||||
const launchServer = (logName = 'server.log') => {
|
||||
const child = spawn(process.execPath, ['-r', fixture.preload, path.join(REPO, 'server.js')], {
|
||||
cwd: REPO, env: { ...process.env, PORT: String(serverPort), CC_WEB_PASSWORD: PASSWORD,
|
||||
CC_WEB_INTERNAL_MCP_TOKEN: MCP_TOKEN,
|
||||
CC_WEB_CONFIG_DIR: path.join(root, 'config'), CC_WEB_SESSIONS_DIR: fixture.sessions, CC_WEB_LOGS_DIR: path.join(root, 'logs'),
|
||||
CODEX_HOME: path.join(fixture.home, '.codex'),
|
||||
CODEX_PATH: fixture.mock, CLAUDE_PATH: path.join(REPO, 'scripts/mock-claude.js'), CC_WEB_TASK_BOARD_ENABLED: '0',
|
||||
}, stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
const log = fs.createWriteStream(path.join(root, logName));
|
||||
child.stdout.pipe(log); child.stderr.pipe(log);
|
||||
child.once('exit', () => log.end());
|
||||
return child;
|
||||
};
|
||||
server = launchServer();
|
||||
await waitFor(async () => (await fetch(`http://127.0.0.1:${serverPort}/`)).ok, '隔离服务启动失败');
|
||||
const mcpResponse = await fetch(`http://127.0.0.1:${serverPort}/api/internal/mcp/stream?sourceSessionId=${SESSION_ID}`, {
|
||||
method: 'POST',
|
||||
@@ -301,6 +321,106 @@ async function main() {
|
||||
}, '登录或会话列表未就绪');
|
||||
await driver.execute(`document.querySelector('.session-item').click();`);
|
||||
evidence.stages.loaded = await capture(driver, 'loaded', root, false);
|
||||
const archiveStart = evidence.protocol.length;
|
||||
fs.renameSync(fixture.rollout, `${fixture.rollout}.isolated-hidden`);
|
||||
rolloutHidden = true;
|
||||
await stopChild(server);
|
||||
server = launchServer('server-restarted.log');
|
||||
await waitFor(async () => {
|
||||
assert.equal(server.exitCode, null, '隔离服务重启后已退出');
|
||||
return (await fetch(`http://127.0.0.1:${serverPort}/`)).ok;
|
||||
}, '归档验证服务重启失败');
|
||||
await driver.refresh();
|
||||
await waitFor(async () => {
|
||||
const ready = await driver.execute('return !document.querySelector("#app").hidden && !!document.querySelector(".session-item")');
|
||||
if (ready) return true;
|
||||
await driver.execute('document.querySelector("#login-password").value=arguments[0];document.querySelector("#login-form").requestSubmit();', [PASSWORD]);
|
||||
return false;
|
||||
}, '归档验证重启后登录或会话列表未就绪');
|
||||
await driver.execute('document.querySelector(".session-item").click();');
|
||||
evidence.stages.archivedAfterRestart = await capture(driver, 'archived-after-restart', root, false);
|
||||
const archivedBeforePaging = await driver.execute(`return {
|
||||
ids: [...document.querySelectorAll('#messages > .msg')].map((node) => node.dataset.messageKey).filter(Boolean),
|
||||
indices: [...document.querySelectorAll('#messages > .msg')]
|
||||
.map((node) => Number(node.dataset.messageIndex))
|
||||
.filter((value) => Number.isFinite(value)),
|
||||
notice: document.querySelector('#history-load-more')?.textContent || '',
|
||||
};`);
|
||||
await driver.execute(`const box=document.querySelector('#messages'); box.scrollTop=0;`);
|
||||
await waitFor(async () => {
|
||||
const state = await driver.execute(`return {
|
||||
earliest: [...document.querySelectorAll('#messages .msg')].some((node) => node.textContent.includes('早期问题 0')),
|
||||
canLoad: !!document.querySelector('.history-load-more-button:not([disabled])'),
|
||||
};`);
|
||||
if (state.earliest) return true;
|
||||
if (state.canLoad) {
|
||||
await driver.execute(`document.querySelector('.history-load-more-button:not([disabled])').click();`);
|
||||
}
|
||||
return false;
|
||||
}, '原始记录移开并重启后,最早消息无法从自身归档读回', 30000);
|
||||
const archived = await driver.execute(`return {
|
||||
ids: [...document.querySelectorAll('#messages > .msg')].map((node) => node.dataset.messageKey).filter(Boolean),
|
||||
indices: [...document.querySelectorAll('#messages > .msg')]
|
||||
.map((node) => Number(node.dataset.messageIndex))
|
||||
.filter((value) => Number.isFinite(value)),
|
||||
notice: document.querySelector('#history-load-more')?.textContent || '',
|
||||
};`);
|
||||
const archiveProtocol = evidence.protocol.slice(archiveStart);
|
||||
const archiveInfo = archiveProtocol.filter((message) => message.type === 'session_info');
|
||||
const archiveChunks = archiveProtocol.filter((message) => message.type === 'session_history_chunk');
|
||||
const archiveMessages = archiveProtocol.flatMap((message) => message.messages || []);
|
||||
const archiveIds = new Set(archiveMessages.map((message) => message.id).filter(Boolean));
|
||||
const archiveIndices = new Set(archiveMessages
|
||||
.map((message) => message.index)
|
||||
.filter((index) => Number.isFinite(index)));
|
||||
assert(archiveInfo.length > 0 && archiveInfo.every((message) => message.historySource === 'archive'),
|
||||
'归档刷新后的 session_info 必须全部来自 archive');
|
||||
assert(archiveInfo.every((message) => message.historyTotal === 148 && message.historyAvailable === true),
|
||||
'归档响应必须提供完整总数,且 historyAvailable=true');
|
||||
assert(archiveChunks.length > 0 && archiveChunks.every((message) => message.historySource === 'archive'),
|
||||
'归档分页必须全部来自 archive');
|
||||
assert(archiveChunks.every((message) => message.historyTotal === 148 && message.historyAvailable === true),
|
||||
'归档分页总数或可用状态不一致');
|
||||
assert.deepEqual(
|
||||
[...archiveIndices].sort((left, right) => left - right),
|
||||
Array.from({ length: 148 }, (_, index) => index),
|
||||
'归档协议消息下标必须连续覆盖 0 到 147',
|
||||
);
|
||||
assert.equal(archiveIds.size, 148, `归档协议应覆盖 148 个稳定 ID,实际 ${archiveIds.size}`);
|
||||
assert.equal(archived.ids.length, 148, `归档消息总数应为 148,实际 ${archived.ids.length}`);
|
||||
assert.deepEqual(archived.ids, fixture.historyIds, '归档分页必须保留夹具全部消息稳定 ID 及顺序');
|
||||
assert.deepEqual(
|
||||
archived.indices.slice().sort((left, right) => left - right),
|
||||
Array.from({ length: archived.indices.length }, (_, index) => index),
|
||||
'归档可见消息下标必须从 0 连续覆盖到 147',
|
||||
);
|
||||
assert.deepEqual(
|
||||
archivedBeforePaging.indices.slice().sort((left, right) => left - right),
|
||||
Array.from({ length: archivedBeforePaging.indices.length }, (_, index) => index + 136),
|
||||
'归档首屏必须从消息下标 136 开始',
|
||||
);
|
||||
assert.equal(new Set(archived.ids).size, 148, '归档必须保留 148 个唯一稳定 ID');
|
||||
assert(!archived.notice.includes('无法恢复') && !archived.notice.includes('待核实'),
|
||||
'完整归档读完后不应保留来源不可用或待核实提示');
|
||||
await driver.execute(`document.querySelector('#messages').scrollTop=0;`);
|
||||
await waitFor(() => driver.execute(`const node=[...document.querySelectorAll('#messages > .msg')]
|
||||
.find((message) => message.dataset.messageKey === arguments[0]);
|
||||
if (!node) return false;
|
||||
const box=node.getBoundingClientRect(), style=getComputedStyle(node);
|
||||
return node.textContent.includes('早期问题 0') && box.top >= 0
|
||||
&& box.bottom <= innerHeight && box.height > 0 && Number(style.opacity) > 0.9;`,
|
||||
[fixture.historyIds[0]]), '归档最早问题没有实际显示在视口');
|
||||
await driver.screenshot(path.join(root, 'archived-paged.png'));
|
||||
evidence.stages.archivedPaged = archived;
|
||||
evidence.archived = {
|
||||
beforePaging: archivedBeforePaging,
|
||||
afterPaging: archived,
|
||||
archiveInfoCount: archiveInfo.length,
|
||||
archiveChunkCount: archiveChunks.length,
|
||||
protocolStableIdCount: archiveIds.size,
|
||||
protocolIndexCount: archiveIndices.size,
|
||||
pagedScreenshot: path.join(root, 'archived-paged.png'),
|
||||
};
|
||||
await driver.execute(`const input=document.querySelector('#msg-input');input.value='继续核对页面职责与流程边界';input.dispatchEvent(new Event('input',{bubbles:true}));document.querySelector('#send-btn').click();`);
|
||||
evidence.stages.running = await capture(driver, 'running', root);
|
||||
await driver.refresh();
|
||||
@@ -323,10 +443,14 @@ async function main() {
|
||||
if (driver?.sessionId) await driver.screenshot(path.join(root, 'failure.png')).catch(() => {});
|
||||
throw new Error(`${error.message}\n证据:${root}`, { cause: error });
|
||||
} finally {
|
||||
fs.writeFileSync(path.join(root, 'evidence.json'), JSON.stringify(evidence, null, 2));
|
||||
if (driver) await driver.close().catch(() => {});
|
||||
if (relay) await relay.close();
|
||||
await stopChild(gecko); await stopChild(server);
|
||||
await stopChild(gecko);
|
||||
await stopChild(server);
|
||||
if (rolloutHidden && fs.existsSync(`${fixture.rollout}.isolated-hidden`)) {
|
||||
fs.renameSync(`${fixture.rollout}.isolated-hidden`, fixture.rollout);
|
||||
}
|
||||
fs.writeFileSync(path.join(root, 'evidence.json'), JSON.stringify(evidence, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user