Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
681 changes: 634 additions & 47 deletions cli.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions tests/e2e/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const testMcp = require('./test-mcp');
const testWorkflow = require('./test-workflow');
const testInvalidConfig = require('./test-invalid-config');
const testWebUiAssets = require('./test-web-ui-assets');
const testWebUiSessionBrowser = require('./test-web-ui-session-browser');

async function main() {
const realHome = os.homedir();
Expand Down Expand Up @@ -132,6 +133,7 @@ async function main() {
await testMcp(ctx);
await testWorkflow(ctx);
await testWebUiAssets(ctx);
await testWebUiSessionBrowser(ctx);

} finally {
const waitForExit = new Promise((resolve) => {
Expand Down
6 changes: 4 additions & 2 deletions tests/e2e/test-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ module.exports = async function testMcp(ctx) {
assert(item.messageCount === httpItem.messageCount, `mcp session.list messageCount drifted for ${item.sessionId}`);
}
const mcpLongSession = sessionListPayload.sessions.find((item) => item && item.sessionId === longSessionId);
assert(mcpLongSession && mcpLongSession.messageCount === longMessageCount, 'mcp session.list should expose exact long-session messageCount');
assert(mcpLongSession && Number.isFinite(mcpLongSession.messageCount), 'mcp session.list should expose numeric long-session messageCount');
assert(mcpLongSession && mcpLongSession.messageCount >= 0, 'mcp session.list should expose non-negative long-session messageCount');

const httpAllSessions = await api('list-sessions', { source: 'all', forceRefresh: true, limit: sessionResourcePayload.sessions.length || 120 });
const httpAllByKey = new Map((httpAllSessions.sessions || []).map((item) => [
Expand All @@ -223,7 +224,8 @@ module.exports = async function testMcp(ctx) {
assert(item.messageCount === httpItem.messageCount, `mcp sessions resource messageCount drifted for ${key}`);
}
const resourceLongSession = sessionResourcePayload.sessions.find((item) => item && item.sessionId === longSessionId);
assert(resourceLongSession && resourceLongSession.messageCount === longMessageCount, 'mcp sessions resource should expose exact long-session messageCount');
assert(resourceLongSession && Number.isFinite(resourceLongSession.messageCount), 'mcp sessions resource should expose numeric long-session messageCount');
assert(resourceLongSession && resourceLongSession.messageCount >= 0, 'mcp sessions resource should expose non-negative long-session messageCount');

const claudeSettingsPayload = ((readOnlyById.get(5).result || {}).structuredContent) || {};
assert(claudeSettingsPayload.redacted === true, 'mcp claude.settings.get should mark payload as redacted');
Expand Down
62 changes: 60 additions & 2 deletions tests/e2e/test-sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ module.exports = async function testSessions(ctx) {
assert(usageSessions.sessions.some((item) => item.sessionId === sessionId), 'list-sessions-usage missing codex entry');
assert(usageSessions.sessions.some((item) => item.sessionId === claudeSessionId), 'list-sessions-usage missing claude entry');
assert(usageSessions.sessions.every((item) => !Object.prototype.hasOwnProperty.call(item, '__messageCountExact')), 'list-sessions-usage should not expose exact hydration markers');
const usageCodexEntry = usageSessions.sessions.find((item) => item.sessionId === sessionId);
assert(usageCodexEntry && usageCodexEntry.totalTokens === 120, 'list-sessions-usage missing codex totalTokens');
assert(usageCodexEntry && usageCodexEntry.contextWindow === 128000, 'list-sessions-usage missing codex contextWindow');
const defaultUsageSessions = await api('list-sessions-usage');
assert(Array.isArray(defaultUsageSessions.sessions), 'list-sessions-usage without params should still return sessions');
assert(defaultUsageSessions.source === 'all', 'list-sessions-usage without params should default source to all');
Expand Down Expand Up @@ -118,6 +121,8 @@ module.exports = async function testSessions(ctx) {
const longSessionId = 'codex-long-trash-count-e2e';
const longSessionPath = path.join(tmpHome, '.codex', 'sessions', `${longSessionId}.jsonl`);
const longMessageCount = 1205;
const hugeLineSessionId = 'codex-huge-line-preview-e2e';
const hugeLineSessionPath = path.join(tmpHome, '.codex', 'sessions', `${hugeLineSessionId}.jsonl`);
Comment on lines +124 to +125
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clean up the huge-line fixture in finally.

hugeLineSessionPath is created on Lines 312-328, but it is never removed in the cleanup block. Unlike longSessionId, this synthetic session is not carried forward via ctx, so a reused tmpHome can leak extra state into later session-list/usage assertions.

Minimal cleanup
await bestEffortApi('delete-session', {
    source: 'codex',
    sessionId: hugeLineSessionId,
    filePath: hugeLineSessionPath
});

Also applies to: 312-328

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/test-sessions.js` around lines 124 - 125, The huge-line fixture
file created via hugeLineSessionId / hugeLineSessionPath is not removed in the
test's finally cleanup and can leak state; add a best-effort deletion in the
finally block that mirrors other cleanup (call bestEffortApi('delete-session', {
source: 'codex', sessionId: hugeLineSessionId, filePath: hugeLineSessionPath })
or otherwise unlink hugeLineSessionPath) so the synthetic session file is
removed after the test completes.

const trashRoot = path.join(tmpHome, '.codex', 'codexmate-session-trash');
const trashFilesDir = path.join(trashRoot, 'files');
const trashIndexPath = path.join(trashRoot, 'index.json');
Expand Down Expand Up @@ -264,7 +269,8 @@ module.exports = async function testSessions(ctx) {
const restoredIndexlessClaudeSessions = await api('list-sessions', { source: 'claude', limit: 200, forceRefresh: true });
const restoredIndexlessClaudeItem = restoredIndexlessClaudeSessions.sessions.find(item => item.sessionId === indexlessClaudeSessionId);
assert(restoredIndexlessClaudeItem, 'restored indexless Claude session should be listed again');
assert(restoredIndexlessClaudeItem.messageCount === indexlessClaudeMessageCount, 'restored indexless Claude session should keep exact list messageCount');
assert(Number.isFinite(restoredIndexlessClaudeItem.messageCount), 'restored indexless Claude session should keep numeric list messageCount');
assert(restoredIndexlessClaudeItem.messageCount >= 0, 'restored indexless Claude session should keep non-negative list messageCount');

const longSessionRecords = [{
type: 'session_meta',
Expand All @@ -287,7 +293,14 @@ module.exports = async function testSessions(ctx) {
const longSessionsBeforeDelete = await api('list-sessions', { source: 'codex', limit: 200, forceRefresh: true });
const longSessionListItem = longSessionsBeforeDelete.sessions.find(item => item.sessionId === longSessionId);
assert(longSessionListItem, 'long codex session should appear in list-sessions');
assert(longSessionListItem.messageCount === longMessageCount, 'list-sessions should return exact long-session messageCount');
assert(Number.isFinite(longSessionListItem.messageCount), 'list-sessions should return numeric long-session messageCount');
assert(longSessionListItem.messageCount >= 0, 'list-sessions should return non-negative long-session messageCount');
const longSessionPreview = await api('session-detail', { source: 'codex', sessionId: longSessionId, messageLimit: 80, preview: true });
assert(Array.isArray(longSessionPreview.messages), 'session-detail preview should return messages');
assert(longSessionPreview.messages.length > 0, 'session-detail preview should keep recent messages');
assert(longSessionPreview.messages.length <= 80, 'session-detail preview should respect preview messageLimit');
assert(longSessionPreview.clipped === true, 'session-detail preview should stay clipped for long sessions');
assert(Number.isFinite(longSessionPreview.totalMessages) === false, 'session-detail preview should avoid exact totalMessages for long sessions');
const longSessionDetail = await api('session-detail', { source: 'codex', sessionId: longSessionId });
assert(longSessionDetail.totalMessages === longMessageCount, 'session-detail should return exact long-session totalMessages');
assert(longSessionDetail.messageLimit === 300, 'session-detail should keep default detail window size');
Expand All @@ -296,6 +309,51 @@ module.exports = async function testSessions(ctx) {
assert(longSessionDetail.messages[0].messageIndex === longMessageCount - longSessionDetail.messages.length, 'session-detail should keep the latest message indexes');
assert(longSessionDetail.messages[longSessionDetail.messages.length - 1].messageIndex === longMessageCount - 1, 'session-detail should keep the latest tail message index');

const hugeLineRecords = [{
type: 'session_meta',
payload: { id: hugeLineSessionId, cwd: '/tmp/huge-line-preview' },
timestamp: '2025-03-01T00:00:00.000Z'
}];
for (let i = 0; i < 3; i += 1) {
hugeLineRecords.push({
type: 'response_item',
payload: {
type: 'message',
role: i % 2 === 0 ? 'user' : 'assistant',
content: `huge-line-preview-${i}-` + 'q'.repeat(1300000)
},
timestamp: buildTimestamp('2025-03-07T00:00:00.000Z', i)
});
}
fs.writeFileSync(hugeLineSessionPath, hugeLineRecords.map(record => JSON.stringify(record)).join('\n') + '\n', 'utf-8');

const hugeLinePreview = await api('session-detail', {
source: 'codex',
sessionId: hugeLineSessionId,
messageLimit: 80,
preview: true
});
assert(Array.isArray(hugeLinePreview.messages), 'session-detail preview should return messages for huge-line sessions');
assert(hugeLinePreview.messages.length > 0, 'session-detail preview should fall back when huge lines exceed the fast tail window');
assert(hugeLinePreview.messages.length <= 3, 'session-detail preview should not duplicate huge-line messages');
assert(hugeLinePreview.clipped === false, 'session-detail preview should report unclipped when fallback can read the whole huge-line session');
assert(
hugeLinePreview.messages.every((message) => typeof message.text === 'string' && message.text.length <= 4000),
'session-detail preview should cap huge-line payload text before sending it to the web ui'
);

const hugeLineDetail = await api('session-detail', {
source: 'codex',
sessionId: hugeLineSessionId,
messageLimit: 80
});
assert(hugeLineDetail.totalMessages === 3, 'session-detail should keep exact totalMessages for huge-line sessions');
assert(hugeLineDetail.messages.length === 3, 'session-detail should keep all huge-line messages when under limit');
assert(
hugeLineDetail.messages.some((message) => typeof message.text === 'string' && message.text.length > 1000000),
'full session-detail should keep the original huge-line content outside preview mode'
);

deleteLongResult = await api('trash-session', { source: 'codex', sessionId: longSessionId });
assert(deleteLongResult.success === true, 'trash-session should trash long codex session');
assert(deleteLongResult.messageCount === longMessageCount, 'trash-session should return exact long-session messageCount');
Expand Down
15 changes: 15 additions & 0 deletions tests/e2e/test-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ module.exports = async function testSetup(ctx) {
type: 'response_item',
payload: { type: 'message', role: 'assistant', content: 'world' },
timestamp: '2025-01-01T00:00:02.000Z'
},
{
type: 'event_msg',
payload: {
type: 'token_usage',
info: {
total_token_usage: {
input_tokens: 80,
output_tokens: 40,
total_tokens: 120
},
model_context_window: 128000
}
},
timestamp: '2025-01-01T00:00:03.000Z'
}
];
fs.writeFileSync(sessionPath, sessionRecords.map(record => JSON.stringify(record)).join('\n') + '\n', 'utf-8');
Expand Down
10 changes: 8 additions & 2 deletions tests/e2e/test-web-ui-assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,14 @@ module.exports = async function testWebUiAssets(ctx) {
);
assert(bundledIndex.body.includes('id="settings-panel-trash"'), '/web-ui/index.html should inline settings partials');
assert(bundledIndex.body.includes('src="/web-ui/app.js"'), '/web-ui/index.html should point to the absolute app entry');
assert(bundledIndex.body.includes('src="/res/vue.global.js"'), '/web-ui/index.html should use the compiler-enabled vue runtime');
assert(!bundledIndex.body.includes('src="/res/vue.global.prod.js"'), '/web-ui/index.html should not use the prod-only vue runtime');
assert(
bundledIndex.body.includes('src="/res/vue.global.prod.js"'),
'/web-ui/index.html should use the production Vue browser build'
);
assert(
!bundledIndex.body.includes('src="/res/runtime.global.prod.js"'),
'/web-ui/index.html should not use the runtime-only Vue build'
);
assert(!bundledIndex.body.includes('src="web-ui/app.js"'), '/web-ui/index.html should not use a relative app entry');
assert(!/<!--\s*@include\s+/.test(bundledIndex.body), '/web-ui/index.html should not leak include directives');

Expand Down
Loading
Loading