__tests__/deploy-readiness.test.js
Rama main · réplica de lectura del repositorio original.
/*
* The gate that protects a production roll, tested without a browser.
*
* Every case below is a state that really happened and was read wrong: a session whose job record
* had aged out blocked a built image for two hours; a parked `idle` conversation counted as work;
* and the log said "1 active session" with three counts already summed, so none of it was visible.
*/
import { describe, it, expect } from 'vitest';
import { readiness, explain } from '../src/deployReadiness.js';
const NOW = 1_700_000_000_000;
const MIN = 60_000;
describe('deploy readiness — what blocks a roll', () => {
it('nothing open means ready', () => {
const r = readiness({}, NOW);
expect(r.busy).toBe(0);
expect(explain(r)).toMatch(/nothing is holding/);
});
it('a session driving a running job blocks', () => {
const r = readiness({ sessions: [{ sessionId: 's-1', profile: 'capcut', lastUsed: NOW,
job: { jobId: 'j-1', role: 'capcut-video-editor', status: 'running' } }] }, NOW);
expect(r.busy).toBe(1);
expect(r.blocking[0].why).toMatch(/driving job j-1 \(capcut-video-editor\) in profile capcut/);
});
it('a session parked on an idle job does NOT block — idle is finished, waiting for a person', () => {
const r = readiness({ sessions: [{ sessionId: 's-mucqqgm0-cgfbcd', profile: 'ghostbrowser',
lastUsed: NOW - 20 * MIN, job: { jobId: 'j-mucqqgm5-irqu5', role: 'learn.shot', status: 'idle' } }] }, NOW);
expect(r.busy).toBe(0);
expect(r.parked).toHaveLength(1);
expect(r.parked[0].why).toMatch(/job j-mucqqgm5-irqu5 is idle, not running/);
});
it.each(['done', 'failed', 'stopped', 'interrupted'])('a session on a %s job does not block', (status) => {
const r = readiness({ sessions: [{ sessionId: 's-1', lastUsed: NOW, job: { jobId: 'j-1', status } }] }, NOW);
expect(r.busy).toBe(0);
});
it('a jobless session touched just now blocks — that is a person in their own browser', () => {
const r = readiness({ sessions: [{ sessionId: 's-1', profile: 'facebook', lastUsed: NOW - 30_000, job: null }] }, NOW);
expect(r.busy).toBe(1);
expect(r.blocking[0].why).toMatch(/someone is probably in it/);
});
it('a jobless session nobody has touched does NOT block — this is the two-hour bug', () => {
/* The job store had aged the record out, so the lookup answered null and the session read
exactly like a person typing in it. Untouched for an hour is not a person. */
const r = readiness({ sessions: [{ sessionId: 's-1', profile: 'ghostbrowser', lastUsed: NOW - 61 * MIN, job: null }] }, NOW);
expect(r.busy).toBe(0);
expect(r.parked[0].why).toMatch(/untouched for 61 min and holding no job/);
});
it('a session with no lastUsed at all is treated as stale, not as busy', () => {
const r = readiness({ sessions: [{ sessionId: 's-1', job: null }] }, NOW);
expect(r.busy).toBe(0);
});
it('a running watcher pass blocks', () => {
expect(readiness({ watchers: ['facebook-post-polish'] }, NOW).busy).toBe(1);
});
it('a recording blocks — a roll would truncate the file', () => {
const r = readiness({ recordings: ['rec-1'] }, NOW);
expect(r.busy).toBe(1);
expect(r.blocking[0].why).toMatch(/truncate/);
});
it('a live assistant turn blocks', () => {
const r = readiness({ chats: [{ id: 'chat-1', title: 'make a viral short', startedAt: NOW - 2 * MIN }] }, NOW);
expect(r.busy).toBe(1);
expect(r.blocking[0].why).toMatch(/started 2 min ago/);
});
it('an assistant turn running longer than any real turn is stuck, not busy', () => {
const r = readiness({ chats: [{ id: 'chat-1', title: 'x', startedAt: NOW - 90 * MIN }] }, NOW);
expect(r.busy).toBe(0);
expect(r.parked[0].why).toMatch(/treating it as stuck/);
});
it('reports the parked holders even when nothing blocks — that sentence was what was missing', () => {
const r = readiness({
sessions: [{ sessionId: 's-1', profile: 'ghostbrowser', lastUsed: NOW - 40 * MIN, job: { jobId: 'j-1', status: 'idle' } }],
chats: [{ id: 'chat-1', title: 'nightly', startedAt: NOW - 120 * MIN }],
}, NOW);
expect(r.busy).toBe(0);
expect(r.holders).toHaveLength(2);
expect(explain(r)).toMatch(/^parked session s-1:/m);
expect(explain(r)).not.toMatch(/BLOCKS/);
});
it('every holder is named, so a log line can never be a bare number again', () => {
const r = readiness({
sessions: [{ sessionId: 's-1', lastUsed: NOW, job: { jobId: 'j-1', status: 'running' } }],
watchers: ['w-1'], recordings: ['rec-1'],
chats: [{ id: 'chat-1', startedAt: NOW }],
}, NOW);
expect(r.busy).toBe(4);
for (const line of explain(r).split('\n')) expect(line).toMatch(/^BLOCKS \w+ [\w-]+: .+/);
});
it('the freshness window is adjustable without editing the rule', () => {
const snap = { sessions: [{ sessionId: 's-1', lastUsed: NOW - 10 * MIN, job: null }] };
expect(readiness(snap, NOW).busy).toBe(0);
expect(readiness(snap, NOW, { freshMs: 15 * MIN }).busy).toBe(1);
});
it('junk in the snapshot is ignored rather than counted as work', () => {
const r = readiness({ sessions: [null, {}, { sessionId: '' }], watchers: [''], recordings: [null], chats: [{}] }, NOW);
expect(r.busy).toBe(0);
expect(r.holders).toHaveLength(0);
});
});