moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { MindDB, CronStore } from '@waggle/core';
import { LocalScheduler } from '../src/local/cron.js';
describe('LocalScheduler', () => {
let tmpDir: string;
let db: MindDB;
let store: CronStore;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-sched-'));
db = new MindDB(path.join(tmpDir, 'test.mind'));
store = new CronStore(db);
});
afterEach(() => {
db.close();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('tick() executes due schedules and returns count', async () => {
const schedule = store.create({
name: 'Test Job',
cronExpr: '*/5 * * * *',
jobType: 'memory_consolidation',
});
// Force next_run_at into the past so it's due
db.getDatabase().prepare(
"UPDATE cron_schedules SET next_run_at = datetime('now', '-1 minute') WHERE id = ?",
).run(schedule.id);
const executed: number[] = [];
const executor = vi.fn(async (s: { id: number }) => {
executed.push(s.id);
});
const scheduler = new LocalScheduler(store, executor);
const count = await scheduler.tick();
expect(count).toBe(1);
expect(executor).toHaveBeenCalledOnce();
expect(executed).toEqual([schedule.id]);
// Verify next_run_at was updated to a future time
const updated = store.getById(schedule.id)!;
expect(updated.last_run_at).not.toBeNull();
expect(new Date(updated.next_run_at!).getTime()).toBeGreaterThan(Date.now() - 60_000);
});
it('tick() with no due schedules returns 0', async () => {
// Create a schedule with future next_run_at (default behavior)
store.create({
name: 'Future Job',
cronExpr: '0 3 * * *',
jobType: 'workspace_health',
});
const executor = vi.fn();
const scheduler = new LocalScheduler(store, executor);
const count = await scheduler.tick();
expect(count).toBe(0);
expect(executor).not.toHaveBeenCalled();
});
it('tick() handles executor failure gracefully', async () => {
const schedule = store.create({
name: 'Failing Job',
cronExpr: '*/5 * * * *',
jobType: 'memory_consolidation',
});
// Force due
db.getDatabase().prepare(
"UPDATE cron_schedules SET next_run_at = datetime('now', '-1 minute') WHERE id = ?",
).run(schedule.id);
const executor = vi.fn(async () => {
throw new Error('Job exploded');
});
const scheduler = new LocalScheduler(store, executor);
// tick() should not throw
const count = await scheduler.tick();
// Failed job should not count as executed
expect(count).toBe(0);
expect(executor).toHaveBeenCalledOnce();
// Schedule should NOT have been marked as run (last_run_at still null)
const after = store.getById(schedule.id)!;
expect(after.last_run_at).toBeNull();
});
it('tick() concurrency guard prevents overlapping ticks', async () => {
const schedule = store.create({
name: 'Slow Job',
cronExpr: '*/5 * * * *',
jobType: 'memory_consolidation',
});
// Force due
db.getDatabase().prepare(
"UPDATE cron_schedules SET next_run_at = datetime('now', '-1 minute') WHERE id = ?",
).run(schedule.id);
let callCount = 0;
const executor = vi.fn(async () => {
callCount++;
// Simulate slow job
await new Promise((resolve) => setTimeout(resolve, 100));
});
const scheduler = new LocalScheduler(store, executor);
// Fire two ticks simultaneously
const [count1, count2] = await Promise.all([
scheduler.tick(),
scheduler.tick(),
]);
// Only one tick should have actually executed the job
expect(count1 + count2).toBe(1);
expect(callCount).toBe(1);
});
it('start/stop lifecycle works', () => {
const executor = vi.fn();
const scheduler = new LocalScheduler(store, executor);
expect(scheduler.isRunning()).toBe(false);
scheduler.start(60_000);
expect(scheduler.isRunning()).toBe(true);
// start again should be a no-op (no double timers)
scheduler.start(60_000);
expect(scheduler.isRunning()).toBe(true);
scheduler.stop();
expect(scheduler.isRunning()).toBe(false);
});
it('isRunning() reflects state correctly', () => {
const executor = vi.fn();
const scheduler = new LocalScheduler(store, executor);
expect(scheduler.isRunning()).toBe(false);
scheduler.start(30_000);
expect(scheduler.isRunning()).toBe(true);
scheduler.stop();
expect(scheduler.isRunning()).toBe(false);
// Can restart
scheduler.start(30_000);
expect(scheduler.isRunning()).toBe(true);
scheduler.stop();
expect(scheduler.isRunning()).toBe(false);
});
});