mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
test(file-viewer): COD-341 execute session lifecycle path
This commit is contained in:
@@ -13,6 +13,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const PUBLIC = resolve(import.meta.dirname, '../src/web/public');
|
||||
const appJs = readFileSync(resolve(PUBLIC, 'app.js'), 'utf8');
|
||||
const constantsJs = readFileSync(resolve(PUBLIC, 'constants.js'), 'utf8');
|
||||
const panelsJs = readFileSync(resolve(PUBLIC, 'panels-ui.js'), 'utf8');
|
||||
const settingsJs = readFileSync(resolve(PUBLIC, 'settings-ui.js'), 'utf8');
|
||||
|
||||
@@ -195,6 +196,126 @@ function loadPanel(options: { sessionId?: string | null; showHidden?: boolean }
|
||||
return { app, elements, pending };
|
||||
}
|
||||
|
||||
function loadRealSelectSessionHarness(options: { terminalFailure?: boolean } = {}) {
|
||||
const elements: Record<string, FakeElement> = {
|
||||
fileBrowserPanel: fakeElement(['visible']),
|
||||
fileBrowserTree: fakeElement(),
|
||||
fileBrowserStatus: fakeElement(),
|
||||
fileBrowserSearch: fakeElement(),
|
||||
fileBrowserExpandBtn: fakeElement(),
|
||||
fileBrowserHiddenBtn: fakeElement(),
|
||||
};
|
||||
const filePending: Array<{ url: string; reply: ReturnType<typeof deferred<FakeResponse>> }> = [];
|
||||
const idleCallbacks: Array<() => void> = [];
|
||||
const context = vm.createContext({
|
||||
console: { ...console, log: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() },
|
||||
performance,
|
||||
setInterval: vi.fn(),
|
||||
clearInterval: vi.fn(),
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
requestAnimationFrame: vi.fn(),
|
||||
requestIdleCallback: (callback: () => void) => {
|
||||
idleCallbacks.push(callback);
|
||||
return idleCallbacks.length;
|
||||
},
|
||||
HTMLCanvasElement: class HTMLCanvasElement {},
|
||||
WebSocket: { OPEN: 1 },
|
||||
MobileDetection: { isTouchDevice: () => false },
|
||||
localStorage: { length: 0, key: vi.fn(), getItem: vi.fn(), setItem: vi.fn(), removeItem: vi.fn() },
|
||||
document: {
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
getElementById: (id: string) => elements[id] ?? null,
|
||||
querySelector: vi.fn(() => null),
|
||||
},
|
||||
window: { addEventListener: vi.fn(), removeEventListener: vi.fn() },
|
||||
fetch: (url: string) => {
|
||||
if (url.includes('/files?')) {
|
||||
const reply = deferred<FakeResponse>();
|
||||
filePending.push({ url, reply });
|
||||
return reply.promise;
|
||||
}
|
||||
if (url.includes('/terminal?')) {
|
||||
if (options.terminalFailure) return Promise.reject(new Error('terminal replay failed'));
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: { get: () => '' },
|
||||
json: async () => ({ data: { terminalBuffer: '', truncated: false, source: 'test' } }),
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
|
||||
},
|
||||
});
|
||||
vm.runInContext(`${constantsJs}\n${appJs}\n${panelsJs}\nglobalThis.__CodemanApp = CodemanApp;`, context);
|
||||
|
||||
const CodemanApp = (context as { __CodemanApp: new () => unknown }).__CodemanApp;
|
||||
const app = Object.create(CodemanApp.prototype) as Record<string, any>;
|
||||
const terminalBoundary = deferred<boolean>();
|
||||
let resizeCalls = 0;
|
||||
app.activeSessionId = 'session/A';
|
||||
app.detachedSessions = new Set();
|
||||
app.isSoloWindow = false;
|
||||
app._selectGeneration = 0;
|
||||
app.sessions = new Map([
|
||||
['session/B', { id: 'session/B', name: 'B', pid: 1, status: 'idle', mode: 'shell', workingDir: '/tmp/B' }],
|
||||
]);
|
||||
app.fileBrowserData = successfulTree('old-A.ts').data;
|
||||
app.fileBrowserExpandedDirs = new Set<string>();
|
||||
app.fileBrowserFilter = '';
|
||||
app.fileBrowserAllExpanded = false;
|
||||
app.fileBrowserShowHidden = false;
|
||||
app.fileBrowserDragListeners = null;
|
||||
app.$ = (id: string) => elements[id] ?? null;
|
||||
app._shouldFocusTerminalForTabSwitch = () => false;
|
||||
app._setTerminalLoadState = vi.fn();
|
||||
app._clearTerminalLoadState = vi.fn();
|
||||
app._cleanupPreviousSession = vi.fn();
|
||||
app._renderHistoryTruncationBanner = vi.fn();
|
||||
app._updateSseSubscription = vi.fn();
|
||||
app.hideWelcome = vi.fn();
|
||||
app.markIdleAlertSeen = vi.fn();
|
||||
app._updateActiveTabImmediate = vi.fn();
|
||||
app.closeSessionSidebarOnHandheld = vi.fn();
|
||||
app.renderSessionTabs = vi.fn();
|
||||
app.updateAttachmentHistoryBadge = vi.fn();
|
||||
app.attachmentHistoryDrawerOpen = false;
|
||||
app._updateLocalEchoState = vi.fn();
|
||||
app._flushedOffsets = new Map();
|
||||
app._flushedTexts = new Map();
|
||||
app._localEchoOverlay = null;
|
||||
app._beginBufferLoad = vi.fn(() => 1);
|
||||
app._isLoadingBuffer = false;
|
||||
app.fitAddon = { fit: vi.fn() };
|
||||
app.sendResize = vi.fn(() => {
|
||||
resizeCalls++;
|
||||
return resizeCalls === 1 ? terminalBoundary.promise : Promise.resolve(false);
|
||||
});
|
||||
app.terminalBufferCache = new Map();
|
||||
app._xtermSnapshots = new Map();
|
||||
app._fullHistoryLoaded = new Set();
|
||||
app._resetTerminalForReplay = vi.fn();
|
||||
app._connectWs = vi.fn();
|
||||
app.scrollToLastNonEmptyLine = vi.fn();
|
||||
app._recordTerminalLoadTiming = vi.fn();
|
||||
app.respawnStatus = {};
|
||||
app.respawnCountdownTimers = {};
|
||||
app.hideRespawnBanner = vi.fn();
|
||||
app.stopCountdownInterval = vi.fn();
|
||||
app.renderRalphStatePanel = vi.fn();
|
||||
app.updateCliInfoDisplay = vi.fn();
|
||||
app.renderProjectInsightsPanel = vi.fn();
|
||||
app.updateSubagentWindowVisibility = vi.fn();
|
||||
app.loadAppSettingsFromStorage = () => ({ showFileBrowser: true });
|
||||
|
||||
const activateImplementation = app._activateFileBrowserSession.bind(app);
|
||||
app._activateFileBrowserSession = vi.fn(activateImplementation);
|
||||
const loadImplementation = app.loadFileBrowser.bind(app);
|
||||
app.loadFileBrowser = vi.fn(loadImplementation);
|
||||
|
||||
return { app, elements, filePending, idleCallbacks, terminalBoundary };
|
||||
}
|
||||
|
||||
async function startSearch(app: Record<string, any>, query = 'widget') {
|
||||
app.filterFileBrowser(query);
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
@@ -891,6 +1012,75 @@ describe('File Viewer server search', () => {
|
||||
expect(firstTerminalAwait).toBeLessThan(retainedIdleLoad);
|
||||
});
|
||||
|
||||
it('executes real selectSession activation before a pending terminal boundary and keeps the B load on failure', async () => {
|
||||
const { app, filePending, terminalBoundary } = loadRealSelectSessionHarness({ terminalFailure: true });
|
||||
|
||||
const selection = app.selectSession('session/B');
|
||||
const immediateState = {
|
||||
activationCalls: app._activateFileBrowserSession.mock.calls.length,
|
||||
activeSessionId: app.activeSessionId,
|
||||
ownerSessionId: app._fileBrowserState?.ownerSessionId,
|
||||
resizeCalls: app.sendResize.mock.calls.length,
|
||||
fileUrls: filePending.map(({ url }) => url),
|
||||
};
|
||||
|
||||
terminalBoundary.reject(new Error('resize failed'));
|
||||
await selection;
|
||||
|
||||
expect(immediateState.activationCalls).toBe(1);
|
||||
expect(app._activateFileBrowserSession).toHaveBeenCalledWith('session/B');
|
||||
expect(immediateState.activeSessionId).toBe('session/B');
|
||||
expect(immediateState.ownerSessionId).toBe('session/B');
|
||||
expect(immediateState.resizeCalls).toBe(1);
|
||||
expect(immediateState.fileUrls).toEqual(['/api/sessions/session%2FB/files?depth=5&showHidden=false']);
|
||||
|
||||
expect(filePending).toHaveLength(1);
|
||||
expect(app._fileBrowserState.ownerSessionId).toBe('session/B');
|
||||
filePending[0].reply.resolve(response(successfulTree('B-after-terminal-failure.ts')));
|
||||
await app._fileBrowserState.treeInFlight.promise;
|
||||
expect(app.fileBrowserData.tree[0].name).toBe('B-after-terminal-failure.ts');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['in-flight request', false],
|
||||
['settled ready state', true],
|
||||
])('the real selectSession idle callback reuses the immediate B %s', async (_case, settleBeforeIdle) => {
|
||||
const { app, elements, filePending, idleCallbacks, terminalBoundary } = loadRealSelectSessionHarness();
|
||||
|
||||
const selection = app.selectSession('session/B');
|
||||
const immediateActivationCalls = app._activateFileBrowserSession.mock.calls.length;
|
||||
const immediateLoadCalls = app.loadFileBrowser.mock.calls.length;
|
||||
const immediateLoad = app.loadFileBrowser.mock.results[0]?.value;
|
||||
const immediateRequestCount = filePending.length;
|
||||
|
||||
if (settleBeforeIdle && filePending[0]) {
|
||||
filePending[0].reply.resolve(response(successfulTree('ready-B.ts')));
|
||||
await immediateLoad;
|
||||
expect(elements.fileBrowserTree.innerHTML).toContain('ready-B.ts');
|
||||
}
|
||||
|
||||
terminalBoundary.resolve(false);
|
||||
await selection;
|
||||
expect(idleCallbacks).toHaveLength(1);
|
||||
idleCallbacks[0]();
|
||||
|
||||
expect(immediateActivationCalls).toBe(1);
|
||||
expect(immediateLoadCalls).toBe(1);
|
||||
expect(immediateRequestCount).toBe(1);
|
||||
expect(app.loadFileBrowser).toHaveBeenCalledTimes(2);
|
||||
expect(app.loadFileBrowser).toHaveBeenLastCalledWith('session/B');
|
||||
expect(filePending).toHaveLength(1);
|
||||
const idleLoad = app.loadFileBrowser.mock.results[1].value;
|
||||
if (settleBeforeIdle) {
|
||||
await idleLoad;
|
||||
expect(app._fileBrowserState.normalState.phase).toBe('ready');
|
||||
} else {
|
||||
expect(idleLoad).toBe(immediateLoad);
|
||||
filePending[0].reply.resolve(response(successfulTree('in-flight-B.ts')));
|
||||
await idleLoad;
|
||||
}
|
||||
});
|
||||
|
||||
it('synchronously resets A state, owns B, and starts one visible B tree load', async () => {
|
||||
const { app, elements, pending } = loadPanel();
|
||||
app.fileBrowserData = successfulTree('old-A.ts').data;
|
||||
|
||||
Reference in New Issue
Block a user