mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +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');
|
const panel = this.$('fileBrowserPanel');
|
||||||
return (
|
return (
|
||||||
state.searchEpoch === request.epoch &&
|
state.searchEpoch === request.epoch &&
|
||||||
|
state.treeEpoch === request.treeEpoch &&
|
||||||
state.ownerSessionId === request.ownerSessionId &&
|
state.ownerSessionId === request.ownerSessionId &&
|
||||||
this.activeSessionId === request.ownerSessionId &&
|
this.activeSessionId === request.ownerSessionId &&
|
||||||
(this.fileBrowserShowHidden === true) === request.showHidden &&
|
(this.fileBrowserShowHidden === true) === request.showHidden &&
|
||||||
@@ -3207,6 +3208,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
|
|
||||||
if (!state.ownerSessionId && this.activeSessionId) state.ownerSessionId = this.activeSessionId;
|
if (!state.ownerSessionId && this.activeSessionId) state.ownerSessionId = this.activeSessionId;
|
||||||
const ownerSessionId = state.ownerSessionId || null;
|
const ownerSessionId = state.ownerSessionId || null;
|
||||||
|
if (!this.activeSessionId || !ownerSessionId || this.activeSessionId !== ownerSessionId) return;
|
||||||
if (!query) {
|
if (!query) {
|
||||||
state.view = 'normal';
|
state.view = 'normal';
|
||||||
state.matches = [];
|
state.matches = [];
|
||||||
@@ -3242,6 +3244,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
epoch: state.searchEpoch,
|
epoch: state.searchEpoch,
|
||||||
|
treeEpoch: state.treeEpoch,
|
||||||
ownerSessionId,
|
ownerSessionId,
|
||||||
showHidden: this.fileBrowserShowHidden === true,
|
showHidden: this.fileBrowserShowHidden === true,
|
||||||
rawInput,
|
rawInput,
|
||||||
|
|||||||
@@ -249,6 +249,49 @@ describe('File Viewer server search', () => {
|
|||||||
expect(pending).toHaveLength(1);
|
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 () => {
|
it('renders flat file and directory matches with captured-owner actions and status', async () => {
|
||||||
const { app, elements, pending } = loadPanel();
|
const { app, elements, pending } = loadPanel();
|
||||||
await settleSearch(
|
await settleSearch(
|
||||||
@@ -295,19 +338,75 @@ describe('File Viewer server search', () => {
|
|||||||
expect(elements.fileBrowserStatus.textContent).toBe('0 matches');
|
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 { app, elements, pending } = loadPanel();
|
const scenarios: Array<{
|
||||||
await startSearch(app);
|
name: string;
|
||||||
elements.fileBrowserPanel.classList.remove('visible');
|
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');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
pending[0].reply.resolve(
|
for (const scenario of scenarios) {
|
||||||
response(successfulData({ matches: [{ name: 'late.ts', path: 'late.ts', type: 'file' }], matchCount: 1 }))
|
const { app, elements, pending } = loadPanel();
|
||||||
);
|
await startSearch(app);
|
||||||
await vi.advanceTimersByTimeAsync(0);
|
expect(app._fileBrowserState.inFlight.treeEpoch, scenario.name).toBe(0);
|
||||||
|
scenario.mutate(app, elements);
|
||||||
|
|
||||||
expect(elements.fileBrowserTree.innerHTML).toContain('Searching');
|
pending[0].reply.resolve(
|
||||||
expect(elements.fileBrowserTree.innerHTML).not.toContain('late.ts');
|
response(
|
||||||
expect(app._fileBrowserState.inFlight).toBeNull();
|
successfulData({
|
||||||
|
matches: [{ name: `late-${scenario.name}.ts`, path: 'late.ts', type: 'file' }],
|
||||||
|
matchCount: 1,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
|
||||||
|
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', () => {
|
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);
|
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 () => {
|
it('accepts 256 trimmed characters', async () => {
|
||||||
const { app, pending } = loadPanel();
|
const { app, pending } = loadPanel();
|
||||||
app.filterFileBrowser(` ${'x'.repeat(256)} `);
|
app.filterFileBrowser(` ${'x'.repeat(256)} `);
|
||||||
|
|||||||
Reference in New Issue
Block a user