mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(file-viewer): COD-341 gate stale search results
This commit is contained in:
@@ -3039,6 +3039,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
const panel = this.$('fileBrowserPanel');
|
||||
return (
|
||||
state.searchEpoch === request.epoch &&
|
||||
state.treeEpoch === request.treeEpoch &&
|
||||
state.ownerSessionId === request.ownerSessionId &&
|
||||
this.activeSessionId === request.ownerSessionId &&
|
||||
(this.fileBrowserShowHidden === true) === request.showHidden &&
|
||||
@@ -3207,6 +3208,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
if (!state.ownerSessionId && this.activeSessionId) state.ownerSessionId = this.activeSessionId;
|
||||
const ownerSessionId = state.ownerSessionId || null;
|
||||
if (!this.activeSessionId || !ownerSessionId || this.activeSessionId !== ownerSessionId) return;
|
||||
if (!query) {
|
||||
state.view = 'normal';
|
||||
state.matches = [];
|
||||
@@ -3242,6 +3244,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
const request = {
|
||||
epoch: state.searchEpoch,
|
||||
treeEpoch: state.treeEpoch,
|
||||
ownerSessionId,
|
||||
showHidden: this.fileBrowserShowHidden === true,
|
||||
rawInput,
|
||||
|
||||
@@ -249,6 +249,49 @@ describe('File Viewer server search', () => {
|
||||
expect(pending).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not schedule when the active session is missing or differs from the retained owner', async () => {
|
||||
const { app, elements, pending } = loadPanel();
|
||||
app._ensureFileBrowserState();
|
||||
|
||||
app.activeSessionId = null;
|
||||
app.filterFileBrowser('missing-active');
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
expect(pending).toHaveLength(0);
|
||||
expect(elements.fileBrowserTree.innerHTML).toBe('');
|
||||
expect(app._fileBrowserState.inFlight).toBeNull();
|
||||
|
||||
app.activeSessionId = 'session/B';
|
||||
app.filterFileBrowser('wrong-active');
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
expect(pending).toHaveLength(0);
|
||||
expect(elements.fileBrowserTree.innerHTML).toBe('');
|
||||
expect(app._fileBrowserState.inFlight).toBeNull();
|
||||
});
|
||||
|
||||
it('cancels the prior debounce and clears a deferred directory target on every input', async () => {
|
||||
const { app, pending } = loadPanel();
|
||||
app._ensureFileBrowserState().deferredDirectoryTarget = {
|
||||
ownerSessionId: 'session/A',
|
||||
path: 'old-directory',
|
||||
};
|
||||
|
||||
app.filterFileBrowser('first query');
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
app._fileBrowserState.deferredDirectoryTarget = {
|
||||
ownerSessionId: 'session/A',
|
||||
path: 'another-directory',
|
||||
};
|
||||
app.filterFileBrowser('second query');
|
||||
|
||||
expect(app._fileBrowserState.deferredDirectoryTarget).toBeNull();
|
||||
await vi.advanceTimersByTimeAsync(249);
|
||||
expect(pending).toHaveLength(0);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(pending.map(({ url }) => url)).toEqual([
|
||||
'/api/sessions/session%2FA/files?depth=5&showHidden=false&q=second%20query',
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders flat file and directory matches with captured-owner actions and status', async () => {
|
||||
const { app, elements, pending } = loadPanel();
|
||||
await settleSearch(
|
||||
@@ -295,19 +338,75 @@ describe('File Viewer server search', () => {
|
||||
expect(elements.fileBrowserStatus.textContent).toBe('0 matches');
|
||||
});
|
||||
|
||||
it('does not render a completion after the panel is hidden and releases its in-flight pointer', async () => {
|
||||
it('rejects deferred results after any captured search context changes', async () => {
|
||||
const scenarios: Array<{
|
||||
name: string;
|
||||
mutate: (app: Record<string, any>, elements: Record<string, FakeElement>) => void;
|
||||
}> = [
|
||||
{
|
||||
name: 'raw input',
|
||||
mutate: (app) => {
|
||||
app._fileBrowserState.filter = 'changed raw input';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search epoch',
|
||||
mutate: (app) => {
|
||||
app._fileBrowserState.searchEpoch++;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'tree epoch',
|
||||
mutate: (app) => {
|
||||
app._fileBrowserState.treeEpoch++;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'owner',
|
||||
mutate: (app) => {
|
||||
app._fileBrowserState.ownerSessionId = 'another-owner';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'active session',
|
||||
mutate: (app) => {
|
||||
app.activeSessionId = 'another-active-session';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'hidden preference',
|
||||
mutate: (app) => {
|
||||
app.fileBrowserShowHidden = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'panel visibility',
|
||||
mutate: (_app, elements) => {
|
||||
elements.fileBrowserPanel.classList.remove('visible');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
const { app, elements, pending } = loadPanel();
|
||||
await startSearch(app);
|
||||
elements.fileBrowserPanel.classList.remove('visible');
|
||||
expect(app._fileBrowserState.inFlight.treeEpoch, scenario.name).toBe(0);
|
||||
scenario.mutate(app, elements);
|
||||
|
||||
pending[0].reply.resolve(
|
||||
response(successfulData({ matches: [{ name: 'late.ts', path: 'late.ts', type: 'file' }], matchCount: 1 }))
|
||||
response(
|
||||
successfulData({
|
||||
matches: [{ name: `late-${scenario.name}.ts`, path: 'late.ts', type: 'file' }],
|
||||
matchCount: 1,
|
||||
})
|
||||
)
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(elements.fileBrowserTree.innerHTML).toContain('Searching');
|
||||
expect(elements.fileBrowserTree.innerHTML).not.toContain('late.ts');
|
||||
expect(app._fileBrowserState.inFlight).toBeNull();
|
||||
expect(elements.fileBrowserTree.innerHTML, scenario.name).toContain('Searching');
|
||||
expect(elements.fileBrowserTree.innerHTML, scenario.name).not.toContain(`late-${scenario.name}.ts`);
|
||||
expect(app._fileBrowserState.inFlight, scenario.name).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('clears a blank query and restores the compatible cached tree immediately without fetching', () => {
|
||||
@@ -329,6 +428,32 @@ describe('File Viewer server search', () => {
|
||||
expect(elements.fileBrowserExpandBtn.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('binds normal-tree previews and encoded downloads to the captured owner', () => {
|
||||
const { app, elements } = loadPanel();
|
||||
app.fileBrowserData = {
|
||||
tree: [
|
||||
{
|
||||
name: 'normal & safe.ts',
|
||||
path: 'src/normal & safe.ts',
|
||||
type: 'file',
|
||||
size: 7,
|
||||
extension: 'ts',
|
||||
},
|
||||
],
|
||||
totalFiles: 1,
|
||||
totalDirectories: 0,
|
||||
truncated: false,
|
||||
};
|
||||
app.renderFileBrowserTree();
|
||||
|
||||
expect(elements.fileBrowserTree.innerHTML).toContain(
|
||||
'href="/api/sessions/session%2FA/file-raw?path=src%2Fnormal%20%26%20safe.ts&download=true"'
|
||||
);
|
||||
app.activeSessionId = 'later-session';
|
||||
elements.fileBrowserTree.querySelectorAll('.file-tree-item')[0].click();
|
||||
expect(app.openFilePreview).toHaveBeenCalledWith('src/normal & safe.ts', 'session/A');
|
||||
});
|
||||
|
||||
it('accepts 256 trimmed characters', async () => {
|
||||
const { app, pending } = loadPanel();
|
||||
app.filterFileBrowser(` ${'x'.repeat(256)} `);
|
||||
|
||||
Reference in New Issue
Block a user