Files
gbrain/test/sync-hard-deadline.test.ts
T
bde11bb18f v0.42.18.0 fix: sync orphan-pileup watchdog (#1633) + links-lag µs stamp (#1768) (#1807)
* fix(extract): links_extraction_lag never clears on Postgres (#1768)

Stamp the full-microsecond updated_at (via to_char ... AT TIME ZONE UTC)
instead of the millisecond-truncated JS Date, so links_extracted_at equals
the DB updated_at exactly and the staleness predicate clears. Stamp SQL
unchanged: version-arm backdating still works, D4 preserved, CDX-1 strengthened.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(core): out-of-band hard-deadline watchdog primitive (#1633)

Bun eval-Worker that SIGTERM->grace->SIGKILLs its own process from a separate
OS thread, so a sync whose main event loop is starved (ReDoS spin) still dies.
Signals SELF (no PID-reuse footgun). Empirically validated on Bun 1.3.13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(sync): arm hard-deadline watchdog + graceful SIGINT cancel (#1633)

cli.ts installs the watchdog before connectEngine (bounds connect hangs);
resolveSyncHardDeadline + composeAbortSignals in sync.ts; SIGINT graceful
cancel on single-source + --all; withRefreshingLock timer unref'd. Non-TTY
default 3600s makes cron orphan-pileup structurally impossible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(claude): annotate process-watchdog + #1768/#1633 fixes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.42.13.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: rebump v0.42.13.0 -> v0.42.18.0 (queue collision)

Sibling workspaces claimed v0.42.13-v0.42.17; advance this branch's slot.
VERSION + package.json + CHANGELOG header + CLAUDE.md annotations + llms bundles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(key-files): current-state phrasing for #1633/#1768 entries (fix check:doc-history)

The doc-history guard bans the bolded **v0.X release-clause marker in reference
docs (history belongs in CHANGELOG + git). Rewrote the extract.ts/sync.ts
additions as current-state prose and de-versioned the process-watchdog entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:00:57 -07:00

91 lines
3.7 KiB
TypeScript

/**
* #1633 wiring: resolveSyncHardDeadline precedence + composeAbortSignals.
* Pure (no engine, no env mutation — env is injected per-call).
*/
import { describe, test, expect } from 'bun:test';
import {
resolveSyncHardDeadline,
composeAbortSignals,
HARD_DEADLINE_GRACE_SEC,
} from '../src/commands/sync.ts';
const GRACE_MS = HARD_DEADLINE_GRACE_SEC * 1000;
describe('resolveSyncHardDeadline', () => {
test('--no-hard-deadline disables everything (even with --timeout)', () => {
const r = resolveSyncHardDeadline(['--source', 'x', '--timeout', '60', '--no-hard-deadline'], { isTty: false });
expect(r).toBeNull();
});
test('--hard-deadline wins and sets the deadline (with grace)', () => {
const r = resolveSyncHardDeadline(['--hard-deadline', '120'], { isTty: true });
expect(r).toEqual({ deadlineMs: 120_000, graceMs: GRACE_MS, reason: 'flag:--hard-deadline' });
});
test('--hard-deadline accepts s/m/h suffix', () => {
expect(resolveSyncHardDeadline(['--hard-deadline', '2m'], { isTty: true })?.deadlineMs).toBe(120_000);
});
test('--hard-deadline with a bad value throws (same posture as --timeout)', () => {
expect(() => resolveSyncHardDeadline(['--hard-deadline', 'nope'], { isTty: true })).toThrow();
expect(() => resolveSyncHardDeadline(['--hard-deadline', '0'], { isTty: true })).toThrow();
});
test('--timeout (single-source) auto-arms the hard backstop', () => {
const r = resolveSyncHardDeadline(['--source', 'briefings', '--timeout', '480'], { isTty: false });
expect(r).toEqual({ deadlineMs: 480_000, graceMs: GRACE_MS, reason: 'flag:--timeout' });
});
test('--timeout + --all does NOT auto-arm (per-source budgets); falls through', () => {
// Non-TTY → falls to the default; TTY → null.
const nonTty = resolveSyncHardDeadline(['--all', '--timeout', '60'], { isTty: false });
expect(nonTty?.reason).toBe('default:non-tty');
const tty = resolveSyncHardDeadline(['--all', '--timeout', '60'], { isTty: true });
expect(tty).toBeNull();
});
test('env GBRAIN_SYNC_MAX_RUNTIME_SECONDS sets the deadline', () => {
const r = resolveSyncHardDeadline([], { isTty: true, env: { GBRAIN_SYNC_MAX_RUNTIME_SECONDS: '900' } });
expect(r).toEqual({ deadlineMs: 900_000, graceMs: GRACE_MS, reason: 'env:GBRAIN_SYNC_MAX_RUNTIME_SECONDS' });
});
test('env 0 disables (overrides the non-TTY default)', () => {
const r = resolveSyncHardDeadline([], { isTty: false, env: { GBRAIN_SYNC_MAX_RUNTIME_SECONDS: '0' } });
expect(r).toBeNull();
});
test('non-TTY default is 3600s', () => {
const r = resolveSyncHardDeadline([], { isTty: false });
expect(r).toEqual({ deadlineMs: 3_600_000, graceMs: GRACE_MS, reason: 'default:non-tty' });
});
test('TTY interactive with no flag/env arms nothing', () => {
expect(resolveSyncHardDeadline([], { isTty: true })).toBeNull();
});
test('defaultNonTtySec override is honored', () => {
const r = resolveSyncHardDeadline([], { isTty: false, defaultNonTtySec: 60 });
expect(r?.deadlineMs).toBe(60_000);
});
});
describe('composeAbortSignals', () => {
test('all-undefined returns undefined', () => {
expect(composeAbortSignals(undefined, undefined)).toBeUndefined();
});
test('single signal is returned directly (no wrapper)', () => {
const c = new AbortController();
expect(composeAbortSignals(c.signal, undefined)).toBe(c.signal);
});
test('composite aborts when ANY input aborts', () => {
const a = new AbortController();
const b = new AbortController();
const sig = composeAbortSignals(a.signal, b.signal)!;
expect(sig.aborted).toBe(false);
b.abort(new Error('boom'));
expect(sig.aborted).toBe(true);
});
});