Skip to content
Merged
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
81 changes: 81 additions & 0 deletions __tests__/cold-start-non-persistent-scan.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ScannerManager } from "../src/scanner-manager";

/**
* Captures what each ConversationScanner was constructed with. The scanner is
* mocked rather than driven for real because the defect is entirely in the
* constructor argument: a persistent scanner reads its own index, and on a
* cold start that index is the only thing it can read.
*/
const hoisted = vi.hoisted(() => ({ ctorArgs: [] as unknown[] }));

vi.mock("@threadbase-sh/scanner", () => ({
ConversationScanner: class {
constructor(options?: unknown) {
hoisted.ctorArgs.push(options);
}
scan() {
return Promise.resolve([]);
}
getMetadataCache() {
return new Map();
}
close() {
return Promise.resolve();
}
},
}));

/** persistenceDisabled:false is the normal case — the flag only flips when cache.open() throws. */
const makeManager = () =>
new ScannerManager({
scanProfiles: [],
codexRoots: [],
directoryDebounceMs: 0,
persistenceDisabled: false,
// No cache at all is the same condition an emptied cache.db produces:
// buildStatCache returns undefined, so nothing forced persistent:false.
cache: () => null,
cacheMonitor: () => null,
projectsRepo: () => null,
conversationsRepo: () => null,
cacheMetadataRepo: () => null,
trackCacheWrite: () => {},
});

describe("cold-start scans never use the scanner's persistent index", () => {
beforeEach(() => {
hoisted.ctorArgs.length = 0;
});

it("builds a non-persistent scanner when there is no stat cache", async () => {
const manager = makeManager();
await manager.get();

expect(hoisted.ctorArgs.length).toBeGreaterThan(0);
for (const args of hoisted.ctorArgs) {
expect(args).toEqual({ persistent: false });
}
await manager.close();
});

it("builds a non-persistent scanner for a full rescan with no stat cache", async () => {
const manager = makeManager();
hoisted.ctorArgs.length = 0;
await manager.getFresh();

expect(hoisted.ctorArgs.length).toBeGreaterThan(0);
for (const args of hoisted.ctorArgs) {
expect(args).toEqual({ persistent: false });
}
await manager.close();
});

it("never constructs a scanner with undefined options, which defaults to persistent", async () => {
const manager = makeManager();
await manager.get();
await manager.getFresh();

expect(hoisted.ctorArgs).not.toContain(undefined);
await manager.close();
});
});
9 changes: 6 additions & 3 deletions src/scanner-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,8 +559,11 @@ export class ScannerManager {
}
this.takeStaleFiles();
const statCache = this.buildStatCache(this.scanner);
// Scanner 0.9.4 reads statCache only in non-persistent scans.
this.scanner = this.newScanner(statCache ? { persistent: false } : undefined);
// Scanner 0.9.4 reads statCache only in non-persistent scans, and the
// streamer never serves from the scanner's persistent index. Letting the
// no-statCache case fall through to the persistent default made an empty
// cache import rows for files deleted long ago (#876).
this.scanner = this.newScanner({ persistent: false });
this.allScanners.add(this.scanner);
this.scannerReady = this.scanner.scan({
...(this.deps.scanProfiles ? { profiles: this.deps.scanProfiles } : {}),
Expand Down Expand Up @@ -609,7 +612,7 @@ export class ScannerManager {
this.takeStaleFiles();
const previous = this.scanner;
const statCache = this.buildStatCache(previous);
const shadow = this.newScanner(statCache ? { persistent: false } : undefined);
const shadow = this.newScanner({ persistent: false });
this.allScanners.add(shadow);
this.scannerReady = shadow.scan({
...(this.deps.scanProfiles ? { profiles: this.deps.scanProfiles } : {}),
Expand Down
7 changes: 3 additions & 4 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1762,10 +1762,9 @@ export class StreamerServer {
// that onConversationChanged invalidations during the scan cannot cause
// getScanner() to restart indefinitely and leave the warm-up stuck.
const warmupStatCache = this.scannerManager.buildStatCache(null);
// Scanner 0.9.4 reads statCache only in non-persistent scans.
const warmupScanner = this.scannerManager.newScanner(
warmupStatCache ? { persistent: false } : undefined,
);
// Scanner 0.9.4 reads statCache only in non-persistent scans, and a
// cold start has none — see the note in ScannerManager.get (#876).
const warmupScanner = this.scannerManager.newScanner({ persistent: false });
this.scannerManager.track(warmupScanner);
// Throttle the per-file onProgress firings to ~one frame per whole
// percent (plus the final tick) so a large scan doesn't flood every
Expand Down