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>
91 lines
3.7 KiB
TypeScript
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);
|
|
});
|
|
});
|