mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* 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>
62 lines
2.5 KiB
TypeScript
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();
|
|
});
|
|
});
|