Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
76 lines
2.6 KiB
JavaScript
76 lines
2.6 KiB
JavaScript
/**
|
|
* Sync-agent activity registry — protects live sync agents from the watchdog
|
|
* orphan-reaper (see services/agent-job/watchdog.js orphan branch).
|
|
*
|
|
* Bug: a sync agent (POST /chat, /run) holds conversations.is_processing=true with
|
|
* no agent_jobs row. During one long blocking call (no intermediate saveStepMessage)
|
|
* updated_at goes stale and the watchdog clears the lock of a STILL-WORKING agent,
|
|
* posting «… перестал отвечать». The registry lets the watchdog tell live from dead.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
|
|
import {
|
|
SYNC_ACTIVITY_MAX_MS,
|
|
markSyncActive,
|
|
markSyncInactive,
|
|
isSyncActive,
|
|
activeSyncCount,
|
|
} from '../chat/agent-execution-shared/sync-activity.js';
|
|
|
|
describe('sync-activity registry', () => {
|
|
beforeEach(() => {
|
|
// Drain any leftover entries between tests (module state is process-global).
|
|
markSyncInactive(101);
|
|
markSyncInactive(202);
|
|
markSyncInactive(303);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('a freshly marked conversation is live → watchdog must skip it', () => {
|
|
expect(isSyncActive(101)).toBe(false); // not yet started
|
|
markSyncActive(101);
|
|
expect(isSyncActive(101)).toBe(true);
|
|
expect(activeSyncCount()).toBe(1);
|
|
});
|
|
|
|
it('accepts string conversation ids (route params arrive as strings)', () => {
|
|
markSyncActive('202');
|
|
expect(isSyncActive(202)).toBe(true); // number lookup
|
|
expect(isSyncActive('202')).toBe(true); // string lookup
|
|
});
|
|
|
|
it('markSyncInactive clears the mark → watchdog may reap', () => {
|
|
markSyncActive(101);
|
|
expect(isSyncActive(101)).toBe(true);
|
|
markSyncInactive(101);
|
|
expect(isSyncActive(101)).toBe(false);
|
|
expect(activeSyncCount()).toBe(0);
|
|
});
|
|
|
|
it('a genuinely hung agent past the max-age cap is NOT protected (and is evicted)', () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-06-04T00:00:00Z'));
|
|
markSyncActive(303);
|
|
expect(isSyncActive(303)).toBe(true);
|
|
|
|
// Just under the cap → still protected.
|
|
vi.advanceTimersByTime(SYNC_ACTIVITY_MAX_MS - 1000);
|
|
expect(isSyncActive(303)).toBe(true);
|
|
|
|
// Past the cap → reapable, and the stale entry is removed.
|
|
vi.advanceTimersByTime(2000);
|
|
expect(isSyncActive(303)).toBe(false);
|
|
expect(activeSyncCount()).toBe(0);
|
|
});
|
|
|
|
it('null/undefined ids are ignored safely', () => {
|
|
expect(() => markSyncActive(null)).not.toThrow();
|
|
expect(() => markSyncInactive(undefined)).not.toThrow();
|
|
expect(isSyncActive(null)).toBe(false);
|
|
expect(isSyncActive(undefined)).toBe(false);
|
|
});
|
|
});
|