Skip to content
Closed
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
32 changes: 17 additions & 15 deletions tests/unit/config-tabs-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ test('config template keeps expected config tabs in top and side navigation', ()
test('web ui script defines provider mode metadata for codex only', () => {
const appScript = readProjectFile('web-ui/app.js');
const configModeComputed = readProjectFile('web-ui/modules/config-mode.computed.mjs');
const sessionTrashMethods = readProjectFile('web-ui/modules/session-trash.methods.mjs');

assert.match(appScript, /CONFIG_MODE_SET/);
assert.match(appScript, /getProviderConfigModeMeta/);
Expand Down Expand Up @@ -225,6 +226,8 @@ test('web ui script defines provider mode metadata for codex only', () => {
assert.doesNotMatch(appScript, /skillsMarketRemoteLatestOnly:\s*true/);
assert.doesNotMatch(appScript, /skillsMarketEcosystems:\s*\[\]/);
assert.match(appScript, /sessionTrashItems:\s*\[\]/);
assert.match(appScript, /createSessionTrashMethods/);
assert.match(appScript, /\.\.\.createSessionTrashMethods\(/);
assert.match(appScript, /sessionTrashVisibleCount:\s*SESSION_TRASH_PAGE_SIZE/);
assert.match(appScript, /sessionTrashTotalCount:\s*0/);
assert.match(appScript, /sessionTrashLoadedOnce:\s*false/);
Expand All @@ -233,21 +236,20 @@ test('web ui script defines provider mode metadata for codex only', () => {
assert.match(appScript, /visibleSessionTrashItems\(\)/);
assert.match(appScript, /sessionTrashHasMoreItems\(\)/);
assert.match(appScript, /sessionTrashHiddenCount\(\)/);
assert.match(appScript, /normalizeSettingsTab\(tab\)/);
assert.match(appScript, /switchSettingsTab\(tab,\s*options = \{\}\)/);
assert.match(appScript, /loadSessionTrash\(options = \{\}\)/);
assert.match(appScript, /loadMoreSessionTrashItems\(\)/);
assert.match(appScript, /restoreSessionTrash\(item\)/);
assert.match(appScript, /purgeSessionTrash\(item\)/);
assert.match(appScript, /clearSessionTrash\(\)/);
assert.match(appScript, /buildSessionTrashItemFromSession\(session,\s*result = \{\}\)/);
assert.match(appScript, /prependSessionTrashItem\(item,\s*options = \{\}\)/);
assert.match(appScript, /resetSessionTrashVisibleCount\(\)/);
assert.match(appScript, /normalizeSessionTrashTotalCount\(totalCount,\s*fallbackItems = this\.sessionTrashItems\)/);
assert.match(appScript, /getSessionTrashViewState\(\)/);
assert.match(appScript, /this\.sessionTrashTotalCount = this\.normalizeSessionTrashTotalCount\(res\.totalCount,\s*nextItems\);/);
assert.match(appScript, /this\.sessionTrashTotalCount = this\.normalizeSessionTrashTotalCount\(\s*res && res\.totalCount !== undefined/);
assert.match(appScript, /messageCount:\s*Number\.isFinite\(Number\(result && result\.messageCount\)\)/);
assert.match(sessionTrashMethods, /normalizeSettingsTab\(tab\)/);
assert.match(sessionTrashMethods, /switchSettingsTab\(tab,\s*options = \{\}\)/);
assert.match(sessionTrashMethods, /loadSessionTrash\(options = \{\}\)/);
assert.match(sessionTrashMethods, /loadMoreSessionTrashItems\(\)/);
assert.match(sessionTrashMethods, /restoreSessionTrash\(item\)/);
assert.match(sessionTrashMethods, /purgeSessionTrash\(item\)/);
assert.match(sessionTrashMethods, /clearSessionTrash\(\)/);
assert.match(sessionTrashMethods, /buildSessionTrashItemFromSession\(session,\s*result = \{\}\)/);
assert.match(sessionTrashMethods, /prependSessionTrashItem\(item,\s*options = \{\}\)/);
assert.match(sessionTrashMethods, /resetSessionTrashVisibleCount\(\)/);
assert.match(sessionTrashMethods, /normalizeSessionTrashTotalCount\(totalCount,\s*fallbackItems = this\.sessionTrashItems\)/);
assert.match(sessionTrashMethods, /getSessionTrashViewState\(\)/);
assert.match(sessionTrashMethods, /this\.sessionTrashTotalCount = this\.normalizeSessionTrashTotalCount\(res\.totalCount,\s*nextItems\);/);
assert.match(sessionTrashMethods, /messageCount:\s*Number\.isFinite\(Number\(result && result\.messageCount\)\)/);
assert.match(appScript, /clearActiveSessionState\(\)/);
assert.match(appScript, /removeSessionFromCurrentList\(session\)/);
assert.match(appScript, /await this\.removeSessionFromCurrentList\(session\);/);
Expand Down
135 changes: 52 additions & 83 deletions tests/unit/session-trash-state.test.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { fileURLToPath, pathToFileURL } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand All @@ -14,6 +14,8 @@ const appSource = fs.readFileSync(appPath, 'utf-8');
const cliSource = fs.readFileSync(cliPath, 'utf-8');
const indexHtmlSource = fs.readFileSync(indexHtmlPath, 'utf-8');
const stylesSource = fs.readFileSync(stylesPath, 'utf-8');
const sessionTrashModule = await import(pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'modules', 'session-trash.methods.mjs')).href);
const { createSessionTrashMethods } = sessionTrashModule;

function escapeRegExp(value) {
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
Expand Down Expand Up @@ -195,6 +197,10 @@ function instantiateFunction(funcSource, funcName, bindings = {}) {
return Function(...bindingNames, `${funcSource}\nreturn ${funcName};`)(...bindingValues);
}

function createSessionTrashTestMethods(api, constants = {}) {
return createSessionTrashMethods({ api, constants });
}

test('buildClaudeSessionIndexEntry prefers normalized metadata when stored index entry is missing or stale', () => {
const buildClaudeStoredIndexMessageCountSource = extractFunctionBySignature(
cliSource,
Expand Down Expand Up @@ -528,12 +534,9 @@ test('upsertClaudeSessionIndexEntry prefers normalized fullPath over stale sessi

test('loadSessionTrashCount ignores stale responses after a newer trash request invalidates them', async () => {
let resolveApi = null;
const loadSessionTrashCountSource = extractMethodAsFunction(appSource, 'loadSessionTrashCount');
const loadSessionTrashCount = instantiateFunction(loadSessionTrashCountSource, 'loadSessionTrashCount', {
api: async () => await new Promise((resolve) => {
resolveApi = resolve;
})
});
const loadSessionTrashCount = createSessionTrashTestMethods(async () => await new Promise((resolve) => {
resolveApi = resolve;
})).loadSessionTrashCount;

const context = {
sessionTrashCountLoading: false,
Expand Down Expand Up @@ -579,18 +582,11 @@ test('loadSessionTrashCount ignores stale responses after a newer trash request
});

test('loadSessionTrashCount trusts a lower authoritative backend totalCount during count-only refresh', async () => {
const normalizeSessionTrashTotalCountSource = extractMethodAsFunction(appSource, 'normalizeSessionTrashTotalCount');
const normalizeSessionTrashTotalCount = instantiateFunction(
normalizeSessionTrashTotalCountSource,
'normalizeSessionTrashTotalCount'
);
const loadSessionTrashCountSource = extractMethodAsFunction(appSource, 'loadSessionTrashCount');
const loadSessionTrashCount = instantiateFunction(loadSessionTrashCountSource, 'loadSessionTrashCount', {
api: async () => ({
totalCount: 0,
items: []
})
});
const normalizeSessionTrashTotalCount = createSessionTrashTestMethods(async () => ({})).normalizeSessionTrashTotalCount;
const loadSessionTrashCount = createSessionTrashTestMethods(async () => ({
totalCount: 0,
items: []
})).loadSessionTrashCount;

const context = {
sessionTrashCountLoading: false,
Expand Down Expand Up @@ -654,19 +650,8 @@ test('loadSessionTrash and loadSessionTrashCount keep independent stale-response
}
resolveList = resolve;
});
const loadSessionTrashCount = instantiateFunction(
extractMethodAsFunction(appSource, 'loadSessionTrashCount'),
'loadSessionTrashCount',
{ api }
);
const loadSessionTrash = instantiateFunction(
extractMethodAsFunction(appSource, 'loadSessionTrash'),
'loadSessionTrash',
{
SESSION_TRASH_LIST_LIMIT: 50,
api
}
);
const loadSessionTrashCount = createSessionTrashTestMethods(api, { sessionTrashListLimit: 50 }).loadSessionTrashCount;
const loadSessionTrash = createSessionTrashTestMethods(api, { sessionTrashListLimit: 50 }).loadSessionTrash;

const context = {
sessionTrashItems: [],
Expand Down Expand Up @@ -731,8 +716,7 @@ test('loadSessionTrash and loadSessionTrashCount keep independent stale-response
});

test('getSessionTrashViewState returns retry when badge count exists but list has never loaded', () => {
const getSessionTrashViewStateSource = extractMethodAsFunction(appSource, 'getSessionTrashViewState');
const getSessionTrashViewState = instantiateFunction(getSessionTrashViewStateSource, 'getSessionTrashViewState');
const getSessionTrashViewState = createSessionTrashTestMethods(async () => ({})).getSessionTrashViewState;

assert.strictEqual(getSessionTrashViewState.call({
sessionTrashLoading: false,
Expand Down Expand Up @@ -761,23 +745,22 @@ test('getSessionTrashViewState returns retry when badge count exists but list ha

test('loadSessionTrash marks latest failures as retryable and clears the failure state after a successful reload', async () => {
let callCount = 0;
const loadSessionTrashSource = extractMethodAsFunction(appSource, 'loadSessionTrash');
const loadSessionTrash = instantiateFunction(loadSessionTrashSource, 'loadSessionTrash', {
SESSION_TRASH_LIST_LIMIT: 50,
api: async () => {
callCount += 1;
if (callCount === 1) {
return { error: 'load failed' };
}
return {
totalCount: 1,
items: [{
trashId: 'trash-1',
sessionId: 'session-1'
}]
};
const loadSessionTrash = createSessionTrashTestMethods(async () => {
callCount += 1;
if (callCount === 1) {
return { error: 'load failed' };
}
});
return {
totalCount: 1,
items: [{
trashId: 'trash-1',
sessionId: 'session-1'
}]
};
}, {
sessionTrashListLimit: 50,
sessionTrashPageSize: 50
}).loadSessionTrash;

const messages = [];
const context = {
Expand Down Expand Up @@ -1740,11 +1723,10 @@ test('deleteSession prefers authoritative trash totalCount from the backend resp
});

test('prependSessionTrashItem prefers authoritative trash totalCount when provided', () => {
const prependSessionTrashItemSource = extractMethodAsFunction(appSource, 'prependSessionTrashItem');
const prependSessionTrashItem = instantiateFunction(prependSessionTrashItemSource, 'prependSessionTrashItem', {
SESSION_TRASH_LIST_LIMIT: 500,
SESSION_TRASH_PAGE_SIZE: 200
});
const prependSessionTrashItem = createSessionTrashTestMethods(async () => ({}), {
sessionTrashListLimit: 500,
sessionTrashPageSize: 200
}).prependSessionTrashItem;

const context = {
sessionTrashItems: [{
Expand Down Expand Up @@ -1905,16 +1887,15 @@ test('restoreSessionPinnedMap normalizes cache without pruning stale entries bef
});

test('loadSessionTrash replays the latest queued refresh after an in-flight request is invalidated', async () => {
const loadSessionTrashSource = extractMethodAsFunction(appSource, 'loadSessionTrash');
const pendingResponses = [];
const apiCalls = [];
const loadSessionTrash = instantiateFunction(loadSessionTrashSource, 'loadSessionTrash', {
SESSION_TRASH_LIST_LIMIT: 500,
api: async (action, params) => await new Promise((resolve) => {
apiCalls.push({ action, params });
pendingResponses.push(resolve);
})
});
const loadSessionTrash = createSessionTrashTestMethods(async (action, params) => await new Promise((resolve) => {
apiCalls.push({ action, params });
pendingResponses.push(resolve);
}), {
sessionTrashListLimit: 500,
sessionTrashPageSize: 200
}).loadSessionTrash;

const context = {
sessionTrashItems: [],
Expand Down Expand Up @@ -1985,26 +1966,14 @@ test('loadSessionTrash replays the latest queued refresh after an in-flight requ

test('session trash restore and purge share the same per-item busy guard', async () => {
const apiCalls = [];
const restoreSessionTrash = instantiateFunction(
extractMethodAsFunction(appSource, 'restoreSessionTrash'),
'restoreSessionTrash',
{
api: async (action) => {
apiCalls.push(action);
return { success: true };
}
}
);
const purgeSessionTrash = instantiateFunction(
extractMethodAsFunction(appSource, 'purgeSessionTrash'),
'purgeSessionTrash',
{
api: async (action) => {
apiCalls.push(action);
return { success: true };
}
}
);
const restoreSessionTrash = createSessionTrashTestMethods(async (action) => {
apiCalls.push(action);
return { success: true };
}).restoreSessionTrash;
const purgeSessionTrash = createSessionTrashTestMethods(async (action) => {
apiCalls.push(action);
return { success: true };
}).purgeSessionTrash;

let confirmCalls = 0;
const context = {
Expand Down
Loading
Loading