Files
gbrain/test/process-watchdog.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

62 lines
2.5 KiB
TypeScript

/**
* Pure-function coverage for the watchdog state machine (#1633). No threads, no
* real timers — the spawn-based integration lives in
* test/process-watchdog.serial.test.ts (Bun-pinned, real processes).
*/
import { describe, test, expect } from 'bun:test';
import { watchdogDecision, installProcessWatchdog } from '../src/core/process-watchdog.ts';
describe('watchdogDecision', () => {
const deadline = 1000;
const grace = 300;
test('waits before the deadline', () => {
expect(watchdogDecision(0, deadline, grace)).toBe('wait');
expect(watchdogDecision(999, deadline, grace)).toBe('wait');
});
test('SIGTERM at the deadline boundary (inclusive)', () => {
expect(watchdogDecision(1000, deadline, grace)).toBe('sigterm');
expect(watchdogDecision(1299, deadline, grace)).toBe('sigterm');
});
test('SIGKILL at deadline+grace boundary (inclusive)', () => {
expect(watchdogDecision(1300, deadline, grace)).toBe('sigkill');
expect(watchdogDecision(5000, deadline, grace)).toBe('sigkill');
});
test('zero grace goes straight to SIGKILL at the deadline', () => {
expect(watchdogDecision(999, deadline, 0)).toBe('wait');
expect(watchdogDecision(1000, deadline, 0)).toBe('sigkill');
});
});
describe('installProcessWatchdog (handle contract)', () => {
test('non-positive deadline returns an inert no-op handle', () => {
const warns: string[] = [];
const h0 = installProcessWatchdog({ deadlineMs: 0, onWarn: (m) => warns.push(m) });
expect(h0.active).toBe(false);
h0.dispose(); // idempotent, no throw
const hNeg = installProcessWatchdog({ deadlineMs: -5, onWarn: (m) => warns.push(m) });
expect(hNeg.active).toBe(false);
});
test('active handle disposes idempotently without killing the test process', () => {
// Long deadline so it never fires during the test; dispose tears it down.
const h = installProcessWatchdog({ deadlineMs: 60_000, graceMs: 60_000, label: 'unit-wd' });
expect(h.active).toBe(true);
h.dispose();
expect(h.active).toBe(false);
h.dispose(); // second dispose is a no-op
expect(h.active).toBe(false);
});
test('label is sanitized to a safe charset', () => {
// A nasty label must not throw at construction (it is stripped before the
// inline worker string). We dispose immediately so nothing fires.
const h = installProcessWatchdog({ deadlineMs: 60_000, label: "evil'; \n process.exit(1) //" });
expect(h.active).toBe(true);
h.dispose();
});
});