Skip to content
Closed
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
2 changes: 1 addition & 1 deletion packages/plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"build:tui": "bun scripts/build-tui.ts",
"check:tui-compiled": "bun run build:tui && git diff --exit-code -- src/tui-compiled && test -z \"$(git status --porcelain -- src/tui-compiled)\"",
"typecheck": "tsc -p ../retina-local-fs/tsconfig.build.json && tsc --noEmit && tsc -p tsconfig.scripts.json",
"test": "bun test",
"test": "bun test --timeout 30000",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The PR's stated first goal is "Enable Bun test-file isolation for plugin tests," but this change removes the --isolate flag that was added earlier in this PR, leaving the test command with no isolation flag (only --timeout 30000). If Bun does not isolate test files into separate processes by default, the shared-state/shared-DB flakiness the PR exists to fix is not actually removed. Either restore --isolate (combined with the timeout) or confirm in the PR description that Bun isolates files by default and that isolation is intentionally dropped.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/plugin/package.json, line 42:

<comment>The PR's stated first goal is "Enable Bun test-file isolation for plugin tests," but this change removes the `--isolate` flag that was added earlier in this PR, leaving the test command with no isolation flag (only `--timeout 30000`). If Bun does not isolate test files into separate processes by default, the shared-state/shared-DB flakiness the PR exists to fix is not actually removed. Either restore `--isolate` (combined with the timeout) or confirm in the PR description that Bun isolates files by default and that isolation is intentionally dropped.</comment>

<file context>
@@ -39,7 +39,7 @@
     "check:tui-compiled": "bun run build:tui && git diff --exit-code -- src/tui-compiled && test -z \"$(git status --porcelain -- src/tui-compiled)\"",
     "typecheck": "tsc -p ../retina-local-fs/tsconfig.build.json && tsc --noEmit && tsc -p tsconfig.scripts.json",
-    "test": "bun test --isolate",
+    "test": "bun test --timeout 30000",
     "lint": "biome check .",
     "lint:fix": "biome check --write .",
</file context>
Suggested change
"test": "bun test --timeout 30000",
"test": "bun test --isolate --timeout 30000",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new --timeout 30000 raises the global per-test timeout from Bun's 5000ms default to 30s for every test file, which contradicts the PR's stated verification that "no global timeouts increased" and masks genuinely hanging tests in CI for 30s each. Set only the specific tests that actually need the longer budget instead of raising the global default, or document which tests justify 30s.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/plugin/package.json, line 42:

<comment>The new `--timeout 30000` raises the global per-test timeout from Bun's 5000ms default to 30s for every test file, which contradicts the PR's stated verification that "no global timeouts increased" and masks genuinely hanging tests in CI for 30s each. Set only the specific tests that actually need the longer budget instead of raising the global default, or document which tests justify 30s.</comment>

<file context>
@@ -39,7 +39,7 @@
     "check:tui-compiled": "bun run build:tui && git diff --exit-code -- src/tui-compiled && test -z \"$(git status --porcelain -- src/tui-compiled)\"",
     "typecheck": "tsc -p ../retina-local-fs/tsconfig.build.json && tsc --noEmit && tsc -p tsconfig.scripts.json",
-    "test": "bun test --isolate",
+    "test": "bun test --timeout 30000",
     "lint": "biome check .",
     "lint:fix": "biome check --write .",
</file context>
Suggested change
"test": "bun test --timeout 30000",
"test": "bun test"

"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write .",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="bun-types" />

import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { afterEach, beforeEach, describe, expect, it, jest } from "bun:test";

import type { RawMessage } from "../../hooks/magic-context/read-session-raw";
import { BOOT_QUIET_MS, setBootQuietPeriodForTests } from "../../plugin/boot-quiet";
Expand Down Expand Up @@ -114,6 +114,7 @@ describe("message-index-async", () => {

afterEach(() => {
setBootQuietPeriodForTests(null);
jest.useRealTimers();
closeQuietly(db);
__resetMessageIndexAsyncForTests();
});
Expand Down Expand Up @@ -359,20 +360,24 @@ describe("message-index-async", () => {
});

it("rebuilds when removal overtakes a boot-quiet reconciliation", async () => {
jest.useFakeTimers();
jest.setSystemTime(0);
const sessionId = "ses-boot-clear";
const surviving = [message("m-survivor", 1, "surviving searchable bytes")];
let reads = 0;
const readSurviving = () => {
reads++;
return surviving;
};
setBootQuietPeriodForTests(Date.now() - BOOT_QUIET_MS + 20);
setBootQuietPeriodForTests(0);

scheduleReconciliation(db, sessionId, readSurviving);
scheduleClearAndReindex(db, sessionId, readSurviving);
const reconciliationFinished = scheduleReconciliation(db, sessionId, readSurviving);
const rebuildFinished = scheduleClearAndReindex(db, sessionId, readSurviving);
expect(isSessionReconciled(sessionId)).toBe(false);

await wait(80);
jest.advanceTimersByTime(BOOT_QUIET_MS);
jest.runAllTimers();
await Promise.all([reconciliationFinished, rebuildFinished]);

expect(reads).toBe(2);
expect(countMessageRows(db, sessionId, "m-survivor")).toBe(1);
Expand Down
71 changes: 40 additions & 31 deletions packages/plugin/src/features/magic-context/message-index-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const INCREMENTAL_DEBOUNCE_MS = 100;
const RECONCILIATION_BATCH_SIZE = 100;

const reconciledSessions = new Set<string>();
const reconciliationScheduledSessions = new Set<string>();
const reconciliationScheduledSessions = new Map<string, Promise<void>>();
const sessionLocks = new Map<string, Promise<void>>();
const incrementalTimers = new Map<string, ReturnType<typeof setTimeout>>();
const pendingIncrementalKeys = new Set<string>();
Expand Down Expand Up @@ -199,23 +199,29 @@ export function scheduleReconciliation(
db: Database,
sessionId: string,
readMessages: ReadMessages,
): void {
if (reconciledSessions.has(sessionId) || reconciliationScheduledSessions.has(sessionId)) {
return;
}
reconciliationScheduledSessions.add(sessionId);

scheduleAfterBootQuiet(() => {
defer(() => {
void reconcileSessionIndex(db, sessionId, readMessages)
.catch((error) => {
logIndexingError(sessionId, "reconciliation", error);
})
.finally(() => {
reconciliationScheduledSessions.delete(sessionId);
});
): Promise<void> {
if (reconciledSessions.has(sessionId)) return Promise.resolve();
const scheduled = reconciliationScheduledSessions.get(sessionId);
if (scheduled) return scheduled;

const completion = new Promise<void>((resolve) => {
scheduleAfterBootQuiet(() => {
defer(() => {
void reconcileSessionIndex(db, sessionId, readMessages)
.catch((error) => {
logIndexingError(sessionId, "reconciliation", error);
})
.finally(() => {
if (reconciliationScheduledSessions.get(sessionId) === completion) {
reconciliationScheduledSessions.delete(sessionId);
}
resolve();
});
});
});
});
reconciliationScheduledSessions.set(sessionId, completion);
return completion;
}

export function scheduleIncrementalIndex(
Expand Down Expand Up @@ -275,26 +281,29 @@ export function scheduleClearAndReindex(
db: Database,
sessionId: string,
readMessages: ReadMessages,
): void {
): Promise<void> {
reconciledSessions.delete(sessionId);
reconciliationScheduledSessions.delete(sessionId);
clearCompletedIncrementalKeys(sessionId);

scheduleAfterBootQuiet(() => {
defer(() => {
void runWithSessionLock(sessionId, () => {
// An older boot-quiet reconciliation can finish after this clear was
// scheduled, so invalidate process state under the same session lock
// that clears the durable index.
reconciledSessions.delete(sessionId);
clearCompletedIncrementalKeys(sessionId);
clearIndexedMessages(db, sessionId);
})
.then(() => reconcileSessionIndex(db, sessionId, readMessages))
.catch((error) => {
return new Promise<void>((resolve) => {
scheduleAfterBootQuiet(() => {
defer(() => {
void runWithSessionLock(sessionId, () => {
// An older boot-quiet reconciliation can finish after this clear was
// scheduled, so invalidate process state under the same session lock
// that clears the durable index.
reconciledSessions.delete(sessionId);
logIndexingError(sessionId, "clear and reindex", error);
});
clearCompletedIncrementalKeys(sessionId);
clearIndexedMessages(db, sessionId);
})
.then(() => reconcileSessionIndex(db, sessionId, readMessages))
.catch((error) => {
reconciledSessions.delete(sessionId);
logIndexingError(sessionId, "clear and reindex", error);
})
.finally(resolve);
});
});
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,30 +62,32 @@ describe("embedding measurement corpus", () => {
const db = openDatabase();
const overflow = 5;
const total = MEASUREMENT_CORPUS_SESSION_ROW_CAP + overflow;
for (let i = 0; i < total; i++) {
recordEmbeddingMeasurement(db, {
sessionId: "ses-cap",
projectPath: "/repo",
// Unique query text per row: dedup is on (query hash, cohort), so
// distinct queries simulate the cohort-transition growth.
queryText: `query ${i}`,
cohortKey: "fp-a:0|fp-b:0",
primaryResultIds: [],
shadowResultIds: [],
primaryLatencyMs: 1,
shadowLatencyMs: 1,
primaryFailed: false,
shadowFailed: false,
primaryModelId: "local-id",
shadowModelId: "synapse-id",
primaryFingerprint: "",
shadowFingerprint: "fp-b",
primaryEpoch: 0,
shadowEpoch: 0,
corpusHash: `corpus-${i}`,
coverage: {},
});
}
db.transaction(() => {
for (let i = 0; i < total; i++) {
recordEmbeddingMeasurement(db, {
sessionId: "ses-cap",
projectPath: "/repo",
// Unique query text per row: dedup is on (query hash, cohort), so
// distinct queries simulate the cohort-transition growth.
queryText: `query ${i}`,
cohortKey: "fp-a:0|fp-b:0",
primaryResultIds: [],
shadowResultIds: [],
primaryLatencyMs: 1,
shadowLatencyMs: 1,
primaryFailed: false,
shadowFailed: false,
primaryModelId: "local-id",
shadowModelId: "synapse-id",
primaryFingerprint: "",
shadowFingerprint: "fp-b",
primaryEpoch: 0,
shadowEpoch: 0,
corpusHash: `corpus-${i}`,
coverage: {},
});
}
})();

const rows = listEmbeddingMeasurements(db, "ses-cap");
expect(rows).toHaveLength(MEASUREMENT_CORPUS_SESSION_ROW_CAP);
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin/src/tui/tui-compiled-runtime-imports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ describe("compiled TUI runtime imports", () => {
* Built from the shared list so the test cannot cover fewer modules than the
* build rewrites. */
async function loadExportSets(): Promise<Record<string, Set<string>>> {
// @opentui/core/testing subclasses a core export during module initialization;
// warm core before the parallel imports so Bun cannot expose its TDZ.
await import("@opentui/core");
const entries = await Promise.all(
TUI_RUNTIME_SPECIFIERS.map(
async (specifier) =>
Expand Down
Loading