maestro/src/user-folder/script-orchestrator.test.ts
oss-sync 5502478636
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (5b6df2f)
2026-06-10 08:40:41 +00:00

159 lines
6.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import type { BrowserSessionRepo } from '../db/browser-session-repo.js';
vi.mock('./script-runner.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./script-runner.js')>();
return { ...actual, runUserScript: vi.fn() };
});
vi.mock('./session-loader.js', () => ({ loadSessionStateForUser: vi.fn() }));
import { runUserScript } from './script-runner.js';
import { loadSessionStateForUser } from './session-loader.js';
import { resolveScriptForKind, resolveAndRunUserScript } from './script-orchestrator.js';
const USER = 'user-1';
let root: string;
function addMacro(name: string, source = 'export default async () => 1;\n'): string {
const dir = join(root, USER, 'browser-macros');
mkdirSync(dir, { recursive: true });
const p = join(dir, name);
writeFileSync(p, source);
return p;
}
const MACRO_WITH_SESSION = `---
description: needs a session
session_profile_id: 7
---
export default async () => 'macro';
`;
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'script-orch-test-'));
vi.clearAllMocks();
vi.mocked(runUserScript).mockResolvedValue({ result: 'ran', logs: ['l1'], durationMs: 12 } as never);
});
afterEach(() => {
rmSync(root, { recursive: true, force: true });
});
describe('resolveScriptForKind', () => {
it('resolves a browser-macro to its path with the playwright runtime', () => {
const p = addMacro('macro.js');
const r = resolveScriptForKind(root, USER, 'macro.js', 'browser-macro');
expect(r).toEqual({ scriptPath: p, subdir: 'browser-macros', runtime: 'playwright' });
});
it('resolves without an explicit kind', () => {
const p = addMacro('macro.js');
const r = resolveScriptForKind(root, USER, 'macro.js', undefined);
expect(r).toMatchObject({ scriptPath: p, subdir: 'browser-macros' });
});
it('returns an error for a missing macro', () => {
const r = resolveScriptForKind(root, USER, 'ghost.js', undefined);
expect(r).toEqual({ error: 'browser-macro not found: browser-macros/ghost.js' });
});
it('treats traversal names as not found instead of escaping', () => {
const r = resolveScriptForKind(root, USER, '../../etc/passwd', undefined);
expect('error' in r).toBe(true);
});
});
describe('resolveAndRunUserScript', () => {
it('runs a macro and appends .js to the name', async () => {
const p = addMacro('task.js');
const r = await resolveAndRunUserScript({ rootDir: root, userId: USER, name: 'task', params: { a: 1 } });
expect(r).toMatchObject({
ok: true, result: 'ran', logs: ['l1'], subdir: 'browser-macros', runtime: 'playwright',
});
expect(runUserScript).toHaveBeenCalledWith(
expect.objectContaining({
scriptPath: p, params: { a: 1 }, runtime: 'playwright', timeoutMs: 60_000,
}),
);
expect(loadSessionStateForUser).not.toHaveBeenCalled();
});
it('returns an error result for an unknown macro', async () => {
const r = await resolveAndRunUserScript({ rootDir: root, userId: USER, name: 'ghost', params: {} });
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toContain('not found');
expect(runUserScript).not.toHaveBeenCalled();
});
it('runs a macro without a session profile directly (no storageState)', async () => {
addMacro('macro.js');
const r = await resolveAndRunUserScript({
rootDir: root, userId: USER, name: 'macro', params: {}, headless: false,
});
expect(r).toMatchObject({ ok: true, subdir: 'browser-macros', runtime: 'playwright' });
expect(runUserScript).toHaveBeenCalledWith(
expect.objectContaining({ storageState: undefined, headless: false }),
);
});
it('fails when a macro declares a session but no repo is configured', async () => {
addMacro('macro.js', MACRO_WITH_SESSION);
const r = await resolveAndRunUserScript({ rootDir: root, userId: USER, name: 'macro', params: {} });
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toContain('BrowserSessionRepo not configured');
expect(runUserScript).not.toHaveBeenCalled();
});
it('propagates session load failures', async () => {
addMacro('macro.js', MACRO_WITH_SESSION);
vi.mocked(loadSessionStateForUser).mockResolvedValue({
ok: false,
error: { kind: 'profile_not_found', message: 'profile 7 not found' },
} as never);
const r = await resolveAndRunUserScript({
rootDir: root, userId: USER, name: 'macro', params: {},
sessRepo: {} as BrowserSessionRepo, masterKeyPath: '/k',
});
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toBe('profile 7 not found');
});
it('hydrates storageState and passes it to the runner', async () => {
addMacro('macro.js', MACRO_WITH_SESSION);
const state = { cookies: [] };
vi.mocked(loadSessionStateForUser).mockResolvedValue({ ok: true, storageState: state } as never);
const r = await resolveAndRunUserScript({
rootDir: root, userId: USER, name: 'macro', params: {},
sessRepo: {} as BrowserSessionRepo, masterKeyPath: '/k', timeoutMs: 5000,
});
expect(r.ok).toBe(true);
expect(loadSessionStateForUser).toHaveBeenCalledWith(
{ sessRepo: {}, masterKeyPath: '/k' }, USER, 7,
);
expect(runUserScript).toHaveBeenCalledWith(
expect.objectContaining({ storageState: state, timeoutMs: 5000 }),
);
});
it('reports a frontmatter parse failure', async () => {
addMacro('macro.js', '---\nsession_profile_id: -2\n---\nbody');
const r = await resolveAndRunUserScript({ rootDir: root, userId: USER, name: 'macro', params: {} });
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toContain('failed to parse script frontmatter');
});
it('never throws when the runner fails — returns ok:false with duration', async () => {
addMacro('boom.js');
vi.mocked(runUserScript).mockRejectedValue(new Error('script exploded'));
const r = await resolveAndRunUserScript({ rootDir: root, userId: USER, name: 'boom', params: {} });
expect(r.ok).toBe(false);
if (!r.ok) {
expect(r.error).toBe('script exploded');
expect(typeof r.durationMs).toBe('number');
}
});
});