159 lines
6.2 KiB
TypeScript
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');
|
|
}
|
|
});
|
|
});
|