mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
fix(jobs): backlinks worker defaults to check, not fix (#1853)
Backlinks Minion jobs submitted with an empty payload (the
sync→embed→backlinks chains enqueued after every ingestion) defaulted to
action='fix', rewriting tracked brain pages with generated "Referenced in"
timeline bullets on every routine run — 129 vault files polluted in one
day on our production brain before we traced it.
This contradicts the documented intent in src/core/cycle.ts
(runPhaseBacklinks): "Maintenance cycles must not rewrite tracked brain
pages with generated 'Referenced in' timeline bullets. [...] the legacy
filesystem fixer remains available explicitly via `gbrain check-backlinks
fix`." — the jobs-worker handler simply inverted that default.
Fix: default to 'check'; 'fix' requires explicit opt-in via
'{"action":"fix"}' (the documented submit shape) or
`gbrain check-backlinks fix`. Both explicit paths are unchanged.
Adds a structural regression test (fix-wave-structural.test.ts precedent)
pinning the default, since the handler dynamically imports
runBacklinksCore and walks a real repo dir — a behavioral test would
require mocking that hides the regression behind a test seam.
Co-authored-by: Valentin Ferriere <valentin@v-labs.fr>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Valentin Ferriere
Claude Opus 4.8
parent
6cf8d8d66c
commit
6cf4f3122d
@@ -1664,7 +1664,13 @@ export async function registerBuiltinHandlers(
|
||||
|
||||
worker.register('backlinks', async (job) => {
|
||||
const { runBacklinksCore } = await import('./backlinks.ts');
|
||||
const action: 'check' | 'fix' = job.data.action === 'check' ? 'check' : 'fix';
|
||||
// Default to 'check', not 'fix': backlinks jobs submitted with an empty
|
||||
// payload (e.g. the sync→embed→backlinks chains enqueued after ingestion)
|
||||
// must never rewrite tracked brain pages with generated "Referenced in"
|
||||
// timeline bullets. Mirrors the documented intent in src/core/cycle.ts
|
||||
// (runPhaseBacklinks). The filesystem fixer stays available explicitly
|
||||
// via '{"action":"fix"}' or `gbrain check-backlinks fix`.
|
||||
const action: 'check' | 'fix' = job.data.action === 'fix' ? 'fix' : 'check';
|
||||
const dir = typeof job.data.dir === 'string'
|
||||
? job.data.dir
|
||||
: (await engine.getConfig('sync.repo_path')) ?? '.';
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Structural regression for the backlinks Minion handler default.
|
||||
*
|
||||
* Backlinks jobs submitted with an EMPTY payload (the sync→embed→backlinks
|
||||
* chains enqueued after every ingestion) must run as 'check', never 'fix'.
|
||||
* The pre-fix handler inverted the default (`=== 'check' ? 'check' : 'fix'`),
|
||||
* so every routine post-ingestion job rewrote tracked brain pages with
|
||||
* generated "Referenced in" timeline bullets — contradicting the documented
|
||||
* intent in src/core/cycle.ts (runPhaseBacklinks): "Maintenance cycles must
|
||||
* not rewrite tracked brain pages with generated 'Referenced in' timeline
|
||||
* bullets."
|
||||
*
|
||||
* Source-grep is the right tool here (see fix-wave-structural.test.ts): the
|
||||
* handler dynamically imports runBacklinksCore and walks a real repo dir, so
|
||||
* a behavioral test would require heavy mocking that hides the regression
|
||||
* behind a test seam. The rule is "this specific default must stay 'check'".
|
||||
*/
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
describe('backlinks Minion handler — empty payload defaults to check, not fix', () => {
|
||||
const src = readFileSync('src/commands/jobs.ts', 'utf8');
|
||||
|
||||
// Isolate the backlinks register block so assertions can't accidentally
|
||||
// match another handler's action parsing.
|
||||
const blockMatch = src.match(
|
||||
/worker\.register\('backlinks',[\s\S]*?runBacklinksCore\(\{[\s\S]*?\}\);/
|
||||
);
|
||||
|
||||
test('the backlinks handler block exists', () => {
|
||||
expect(blockMatch).not.toBeNull();
|
||||
});
|
||||
|
||||
test("default action is 'check' (explicit opt-in required for 'fix')", () => {
|
||||
const block = blockMatch![0];
|
||||
expect(block).toMatch(
|
||||
/job\.data\.action\s*===\s*'fix'\s*\?\s*'fix'\s*:\s*'check'/
|
||||
);
|
||||
});
|
||||
|
||||
test('the inverted (fix-by-default) shape stays absent', () => {
|
||||
const block = blockMatch![0];
|
||||
expect(block).not.toMatch(
|
||||
/job\.data\.action\s*===\s*'check'\s*\?\s*'check'\s*:\s*'fix'/
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user