Files
gbrain/test/remediation-run-loop.serial.test.ts
b35c617252 reland: fix(onboard): stop repeating the same auto-remediation within a run (#2854) (#3342)
* fix(onboard): stop repeating the same auto-remediation within a run (#2854)

When the recommendation list is refreshed between remediation steps, a
remediation that doesn't clear its own health signal is reintroduced
under its stable id and attempted again, indefinitely on long runs.
Track attempted recommendation ids for the run and skip re-attempts.

Includes a behavioral regression test: a persistently-stuck signal is
attempted once, the loop terminates, and other remediations still run.

* fix(test): quarantine remediation-run-loop test as serial + complete BrainHealth fixture

Two CI failures, one root cause each:
- verify (check:test-isolation + typecheck): the new test uses mock.module
  (R2) so it must live in the *.serial.test.ts quarantine, and the
  BrainHealth fixture was missing the now-required linkable_page_count.
- test (6): the top-level mock.module('../src/core/ai/gateway.ts') leaked
  into other files in the parallel shard process, flaking
  test/ai/adaptive-embed-batch.test.ts. Serial quarantine fixes it —
  run-serial-tests.sh executes each serial file in its own bun process.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Sanchal Ranjan <84386862+sanchalr@users.noreply.github.com>
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 12:11:29 -07:00

81 lines
2.4 KiB
TypeScript

import { describe, expect, mock, test } from 'bun:test';
import type { BrainEngine } from '../src/core/engine.ts';
import type { BrainHealth } from '../src/core/types.ts';
const attemptedJobs: string[] = [];
mock.module('../src/core/minions/queue.ts', () => ({
MinionQueue: class {
async add(name: string) {
attemptedJobs.push(name);
return { id: attemptedJobs.length };
}
},
}));
mock.module('../src/core/minions/wait-for-completion.ts', () => ({
waitForCompletion: async (_queue: unknown, jobId: number) => ({
id: jobId,
status: 'completed',
}),
}));
mock.module('../src/core/remediation-checkpoint.ts', () => ({
computePlanHash: (ids: string[]) => [...ids].sort().join('|'),
saveRemediationCheckpoint: () => undefined,
loadRemediationCheckpoint: () => null,
listRemediationCheckpoints: () => [],
clearRemediationCheckpoint: () => undefined,
}));
mock.module('../src/core/ai/gateway.ts', () => ({
getEmbeddingModel: () => 'ollama:nomic-embed-text',
getEmbeddingDimensions: () => 768,
withBudgetTracker: async (_tracker: unknown, fn: () => Promise<void>) => fn(),
}));
const { runRemediation } = await import('../src/core/remediation/run.ts');
function makeHealth(): BrainHealth {
return {
page_count: 100,
linkable_page_count: 100,
embed_coverage: 1,
stale_pages: 1,
orphan_pages: 0,
missing_embeddings: 0,
brain_score: 80,
dead_links: 1,
link_coverage: 1,
timeline_coverage: 1,
most_connected: [],
embed_coverage_score: 35,
link_density_score: 25,
timeline_coverage_score: 15,
no_orphans_score: 15,
no_dead_links_score: 0,
};
}
describe('runRemediation recheck loop guard', () => {
test('attempts a stable stuck remediation once and continues to later work', async () => {
attemptedJobs.length = 0;
const health = makeHealth();
const engine = {
kind: 'postgres',
getHealth: async () => health,
getConfig: async (key: string) => key === 'sync.repo_path' ? '/brain' : null,
} as BrainEngine;
const result = await runRemediation(engine, { maxJobs: 4 });
expect(attemptedJobs.filter((name) => name === 'backlinks')).toHaveLength(1);
expect(attemptedJobs).toEqual(['backlinks', 'sync', 'extract']);
expect(result.submitted.map((step) => step.id)).toEqual([
'backlinks.fix',
'sync.repo',
'extract.all',
]);
});
});