mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
fix: flag a Files panel folder that did not answer the probe (release review)
A subfolder whose bounded probe answered `unknown` came back as `children: []` with no `incomplete` and `truncated` false, so a dead mount, or a healthy one slower than the 1.5 s probe on first touch, read as an empty folder and a search missed it in silence. `unknown` is never `absent` (#516). Such a folder now goes on the walk's `unread` list, which flags the node and sets `truncated` in both walks. A folder the filesystem says is gone drops out quietly as before. The row said "file limit reached", which would be false here, so it now reads "Not fully scanned" for either reason. Reproduced on a real disk with a parent at mode 0444, where the child's stat answers EACCES: the API, the search and the panel all report it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2f4952c34
commit
37dfb73b1e
9 files changed
+55
-26
No files matched your search
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* @fileoverview The Files panel's row for a folder the scan's file limit stopped short of.
|
||||
* @fileoverview The Files panel's row for a folder the scan stopped short of.
|
||||
*
|
||||
* `GET .../files` gives a directory its node before reading it, so a folder still queued when the
|
||||
* budget ends has `children: []` or a partial list (test/routes/file-search-mode.test.ts). Without
|
||||
* budget ends, or one that did not answer, has `children: []` or a partial list
|
||||
* (test/routes/file-search-mode.test.ts, test/routes/file-walk-bounds.test.ts). Without
|
||||
* `incomplete` that reads as an empty folder. What can go wrong here is the client: the flag lost
|
||||
* on the way in, the row drawn for a collapsed or finished folder, or the click wiring treating it
|
||||
* as a file.
|
||||
@@ -76,7 +77,7 @@ const render = (app: Record<string, any>, tree: unknown[]) => {
|
||||
return [...treeEl().querySelectorAll('.file-tree-incomplete')] as HTMLElement[];
|
||||
};
|
||||
|
||||
describe('Files panel, a folder the file limit stopped short of', () => {
|
||||
describe('Files panel, a folder the scan stopped short of', () => {
|
||||
it('keeps the flag through the envelope check, which ignores what it does not know', () => {
|
||||
const app = loadPanel();
|
||||
const data = app._validateFileBrowserTreeEnvelope(envelope([bulk, later]));
|
||||
@@ -89,7 +90,7 @@ describe('Files panel, a folder the file limit stopped short of', () => {
|
||||
const rows = render(loadPanel(['a-bulk']), [bulk, later]);
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].textContent).toBe('Not fully scanned (file limit reached)');
|
||||
expect(rows[0].textContent).toBe('Not fully scanned');
|
||||
expect(rows[0].dataset.depth).toBe('1');
|
||||
const items = [...treeEl().children].map((el) => (el as HTMLElement).dataset.path ?? 'ROW');
|
||||
expect(items).toEqual(['a-bulk', 'a-bulk/one.txt', 'ROW', 'z-later']);
|
||||
|
||||
@@ -143,9 +143,11 @@ describe('files endpoint read bounds', () => {
|
||||
it('answers without it, and costs it one stuck stat however often the panel asks', async () => {
|
||||
const first = await deadline(get(''));
|
||||
|
||||
// Nothing was read from it, so `children: []` alone would pass it off as an empty folder (#516:
|
||||
// unknown is never absent).
|
||||
const dead = first.tree.find((node: { name: string }) => node.name === 'z-nas');
|
||||
expect(dead).toEqual({ name: 'z-nas', path: 'z-nas', type: 'directory', children: [] });
|
||||
expect(first.truncated).toBe(false);
|
||||
expect(dead).toEqual({ name: 'z-nas', path: 'z-nas', type: 'directory', children: [], incomplete: true });
|
||||
expect(first.truncated).toBe(true);
|
||||
expect(first.tree[0].children.map((node: { name: string }) => node.name)).toEqual(['needle.ts']);
|
||||
expect(deadReads).toBe(0);
|
||||
|
||||
@@ -153,8 +155,24 @@ describe('files endpoint read bounds', () => {
|
||||
const stats = statCalls();
|
||||
const search = await deadline(get('?q=needle'));
|
||||
expect(search.matches.map((match: { path: string }) => match.path)).toEqual([join('a-fine', 'needle.ts')]);
|
||||
// A hit could be behind it, which is what the search's own `truncated` is for.
|
||||
expect(search.truncated).toBe(true);
|
||||
expect(statCalls()).toBe(stats);
|
||||
expect(deadReads).toBe(0);
|
||||
});
|
||||
|
||||
it('leaves a folder alone once the filesystem says it is gone: that is not an unfinished scan', async () => {
|
||||
vi.mocked(fs.stat).mockImplementation((async (path: string) => {
|
||||
if (path === deadFolder) throw Object.assign(new Error('gone'), { code: 'ENOENT' });
|
||||
return { size: 1, isDirectory: () => true };
|
||||
}) as never);
|
||||
|
||||
const data = await deadline(get(''));
|
||||
|
||||
const gone = data.tree.find((node: { name: string }) => node.name === 'z-nas');
|
||||
expect(gone).toEqual({ name: 'z-nas', path: 'z-nas', type: 'directory', children: [] });
|
||||
expect(data.truncated).toBe(false);
|
||||
expect(deadReads).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user