test: assert the v1 gate at real entry points, including the missing-dep guard

Adds the integration-level checks the leaf tests could not give. Every previous round's
tests exercised a mechanism by handing it the token it needed, which passes for a broken
system; these drive the actual entry points instead.

Covers: default-off when the admin never opted in AND that the engine does not merely
refuse to fire but never arms; explicit false stays off; explicit opt-in (boolean and
object-with-limits) turns on; Run Now refuses under the SCHEDULES_DISABLED lever; Run Now
refuses when the BASE config disables it even though a principal-merged view re-enables.

Also asserts the construction-time dep validation throws for a missing dep — the exact
class of failure (the JS adapter is not typechecked against SchedulesServiceDeps) that
shipped the deletion-barrier probe unwired twice.
This commit is contained in:
Danny Avila 2026-07-24 15:57:30 -04:00
parent d3f487abfb
commit 543a65b954

View file

@ -0,0 +1,100 @@
import type { SchedulesServiceDeps } from './service';
import { createSchedulesService } from './service';
jest.mock('../stream/GenerationJobManager', () => ({
GenerationJobManager: { getJobStore: () => null, abortJob: jest.fn(), isRedis: false },
}));
type Cfg = { interfaceConfig?: { schedules?: unknown } };
function makeService(base: Cfg, merged: Cfg = base) {
const deps = {
methods: {
countActiveRuns: jest.fn(async () => 0),
getCapacityOccupancy: jest.fn(async () => ({ takenSlots: [], unslotted: 0 })),
ensureScheduleIndexes: jest.fn(async () => undefined),
acquireManualRunLease: jest.fn(async () => null),
},
getAppConfig: jest.fn(async (options?: { baseOnly?: boolean }) =>
options?.baseOnly === true ? base : merged,
),
findUserById: jest.fn(async () => null),
findBalance: jest.fn(async () => null),
upsertBalance: jest.fn(async () => null),
resolveAgentFireAccess: jest.fn(async () => 'ok' as const),
isUserDeleting: jest.fn(async () => false),
} as unknown as SchedulesServiceDeps;
return createSchedulesService(deps);
}
const schedule = { id: 's1', user: 'u1' } as never;
const limits = {
enabled: true,
maxPerUser: 10,
minIntervalMinutes: 60,
autoDisableAfterFailures: 5,
fireConcurrency: 5,
};
describe('v1 experimental gate, asserted at real entry points', () => {
afterEach(() => {
delete process.env.SCHEDULES_DISABLED;
});
it('is OFF when the admin never opted in, and does not arm the engine', async () => {
const service = makeService({});
expect((await service.getLimits()).enabled).toBe(false);
// The engine must not merely refuse to fire — it must not start at all.
expect(await service.initializeScheduleEngine()).toBeUndefined();
});
it('stays OFF for an explicit false', async () => {
expect((await makeService({ interfaceConfig: { schedules: false } }).getLimits()).enabled).toBe(
false,
);
});
it('turns ON for an explicit opt-in', async () => {
expect((await makeService({ interfaceConfig: { schedules: true } }).getLimits()).enabled).toBe(
true,
);
const tuned = makeService({ interfaceConfig: { schedules: { maxPerUser: 3 } } });
const resolved = await tuned.getLimits();
expect(resolved.enabled).toBe(true);
expect(resolved.maxPerUser).toBe(3);
});
it('REFUSES a manual run-now while the global kill switch is on', async () => {
// Run Now dispatches the same billed generation as an automatic fire, so gating only
// the engine tick would leave this path open.
process.env.SCHEDULES_DISABLED = 'true';
const service = makeService({ interfaceConfig: { schedules: true } });
const result = await service.fireScheduleNow(schedule, limits);
expect(result).toEqual({ fired: false, skipped: 'disabled' });
});
it('refuses run-now when the BASE config disables it, even if a principal re-enables', async () => {
const service = makeService(
{ interfaceConfig: { schedules: false } },
{ interfaceConfig: { schedules: true } },
);
const result = await service.fireScheduleNow(schedule, limits);
expect(result).toEqual({ fired: false, skipped: 'disabled' });
});
it('constructing the service without a required dep fails LOUDLY at boot', () => {
// The JS adapter is not typechecked against SchedulesServiceDeps, which is how the
// deletion-barrier probe shipped unwired twice. A missing dep must not surface as a
// cryptic per-fire "is not a function".
expect(() =>
createSchedulesService({
methods: {},
getAppConfig: jest.fn(),
findUserById: jest.fn(),
findBalance: jest.fn(),
upsertBalance: jest.fn(),
resolveAgentFireAccess: jest.fn(),
} as unknown as SchedulesServiceDeps),
).toThrow(/isUserDeleting/);
});
});