87 lines
7.2 KiB
TypeScript
87 lines
7.2 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { parseSyncEnqueueBody } from '../src/lib/client/sync-queue';
|
|
import { workerEnvironment } from '../scripts/run-sync-worker';
|
|
import { prepareEnrichmentChunks } from '../workers/sync';
|
|
|
|
describe('T07 worker runtime boundary', () => {
|
|
test('bundled worker starts with inert restricted configuration and has no unresolved aliases', async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), 'eve-sync-worker-'));
|
|
try {
|
|
const output = join(directory, 'sync-worker.js');
|
|
const built = await Bun.build({ entrypoints: ['workers/sync.ts'], target: 'bun', outdir: directory, naming: 'sync-worker.js', minify: false });
|
|
expect(built.success).toBe(true);
|
|
const source = await readFile(output, 'utf8');
|
|
expect(source.includes('$lib/')).toBe(false);
|
|
const env: Record<string, string | undefined> = { PATH: process.env.PATH, HOME: process.env.HOME, CONVEX_URL: 'http://127.0.0.1:9', CONVEX_SYNC_WORKER_SECRET: 'inert-worker-secret', TOKEN_ENCRYPTION_KEY: Buffer.alloc(32).toString('base64'), EVE_CLIENT_ID: 'inert-client', EVE_CALLBACK_URL: 'http://localhost/unused-worker-callback', ESI_USER_AGENT: 'EVE-Intelligence/test', SYNC_WORKER_STARTUP_CHECK: '1' };
|
|
for (const name of ['CONVEX_BRIDGE_SECRET', 'CONVEX_SELF_HOSTED_ADMIN_KEY', 'APP_AUTH_PRIVATE_KEY', 'APP_AUTH_JWKS', 'TYPESAFE_API_KEY']) delete env[name];
|
|
const child = Bun.spawn(['bun', output], { env, stdout: 'pipe', stderr: 'pipe' });
|
|
const code = await child.exited;
|
|
const outputText = await new Response(child.stdout).text() + await new Response(child.stderr).text();
|
|
expect(code).toBe(0);
|
|
expect(outputText).toBe('');
|
|
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('worker preflights oversized enrichment envelopes before any stage mutation', () => {
|
|
const rows = Array.from({ length: 100 }, (_, index) => ({ itemId: String(index + 1), payload: 'x'.repeat(6_000) })) as any;
|
|
const chunks = prepareEnrichmentChunks(rows);
|
|
expect(chunks).not.toBeNull();
|
|
expect(chunks!.length).toBeGreaterThan(1);
|
|
expect(Math.max(...chunks!.map((chunk) => new TextEncoder().encode(JSON.stringify(chunk)).byteLength))).toBeLessThanOrEqual(256 * 1024);
|
|
expect(prepareEnrichmentChunks([{ payload: 'x'.repeat(300_000) }] as any)).toBeNull();
|
|
expect(prepareEnrichmentChunks(Array.from({ length: 2_500 }, () => ({ payload: 'x'.repeat(1_000) })) as any)).toBeNull();
|
|
});
|
|
|
|
test('client preserves structured all-skipped and partial enqueue outcomes on HTTP errors', async () => {
|
|
expect(parseSyncEnqueueBody({ queued: 0, jobs: [], skipped: [{ characterId: 1, reason: 'queue_full' }], partial: true })).toMatchObject({ queued: 0, skipped: [{ characterId: 1, reason: 'queue_full' }] });
|
|
expect(parseSyncEnqueueBody({ queued: 1, jobs: [{ id: 'job', characterId: 2, status: 'queued', attempts: 0, dueAt: 1, createdAt: 1, updatedAt: 1, leaseExpiresAt: null, cancelRequested: false }], skipped: [{ characterId: 3, reason: 'enqueue_failed' }], partial: true })).toMatchObject({ queued: 1, jobs: [{ id: 'job', characterId: 2 }], skipped: [{ characterId: 3, reason: 'enqueue_failed' }] });
|
|
const originalFetch = globalThis.fetch;
|
|
try {
|
|
globalThis.fetch = (async () => Response.json({ queued: 0, jobs: [], skipped: [{ characterId: 1, reason: 'queue_full' }], partial: true }, { status: 429 })) as unknown as typeof fetch;
|
|
const allSkipped = await (await import('../src/lib/client/sync-queue')).enqueueSync();
|
|
expect(allSkipped.response.status).toBe(429); expect(allSkipped.body).toMatchObject({ jobs: [], skipped: [{ reason: 'queue_full' }] });
|
|
globalThis.fetch = (async () => Response.json({ queued: 1, jobs: [{ id: 'job', characterId: 2, status: 'queued', attempts: 0, dueAt: 1, createdAt: 1, updatedAt: 1, leaseExpiresAt: null, cancelRequested: false }], skipped: [{ characterId: 3, reason: 'enqueue_failed' }], partial: true }, { status: 503 })) as unknown as typeof fetch;
|
|
const partial = await (await import('../src/lib/client/sync-queue')).enqueueSync();
|
|
expect(partial.response.status).toBe(503); expect(partial.body).toMatchObject({ jobs: [{ id: 'job' }], skipped: [{ reason: 'enqueue_failed' }] });
|
|
} finally { globalThis.fetch = originalFetch; }
|
|
});
|
|
|
|
test('missing status is surfaced and status polling forwards cancellation', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
try {
|
|
globalThis.fetch = (async () => new Response(null, { status: 404 })) as unknown as typeof fetch;
|
|
expect(await (await import('../src/lib/client/sync-queue')).syncJobStatus('missing')).toBeNull();
|
|
globalThis.fetch = (() => new Promise<Response>(() => {})) as unknown as typeof fetch;
|
|
await expect((await import('../src/lib/client/sync-queue')).syncJobStatus('never', undefined, 5)).rejects.toThrow('timed out');
|
|
const controller = new AbortController();
|
|
globalThis.fetch = ((_input: RequestInfo | URL, init?: RequestInit) => new Promise<Response>((_resolve, reject) => {
|
|
init?.signal?.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')), { once: true });
|
|
})) as unknown as typeof fetch;
|
|
const pending = (await import('../src/lib/client/sync-queue')).syncJobStatus('running', controller.signal);
|
|
controller.abort();
|
|
await expect(pending).rejects.toMatchObject({ name: 'AbortError' });
|
|
} finally { globalThis.fetch = originalFetch; }
|
|
});
|
|
|
|
test('worker child environment drops Infisical bootstrap credentials and ambient web keys', () => {
|
|
const child = workerEnvironment({ PATH: '/bin', CONVEX_URL: 'http://inert', CONVEX_SYNC_WORKER_SECRET: 'worker-only', TOKEN_ENCRYPTION_KEY: 'inert-key', EVE_CLIENT_ID: 'inert-client', EVE_CLIENT_SECRET: 'inert-eve-secret', REFERENCE_DIR: '/app/data/reference', INFISICAL_TOKEN: 'inert-token', INFISICAL_UNIVERSAL_AUTH_CLIENT_ID: 'inert-client-id', INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET: 'inert-client-secret', CONVEX_BRIDGE_SECRET: 'inert-bridge', APP_AUTH_PRIVATE_KEY: 'inert-jwt', TYPESAFE_API_KEY: 'inert-jev' });
|
|
expect(child).toEqual({ PATH: '/bin', CONVEX_URL: 'http://inert', CONVEX_SYNC_WORKER_SECRET: 'worker-only', TOKEN_ENCRYPTION_KEY: 'inert-key', EVE_CLIENT_ID: 'inert-client', EVE_CLIENT_SECRET: 'inert-eve-secret', REFERENCE_DIR: '/app/data/reference' });
|
|
expect(Object.keys(child).some((key) => key.startsWith('INFISICAL_'))).toBe(false);
|
|
});
|
|
|
|
test('direct worker launcher refuses general bridge/admin/JWT/JEV credentials', async () => {
|
|
for (const name of ['CONVEX_BRIDGE_SECRET', 'CONVEX_SELF_HOSTED_ADMIN_KEY', 'APP_AUTH_PRIVATE_KEY', 'APP_AUTH_JWKS', 'TYPESAFE_API_KEY']) {
|
|
const forbidden = `inert-${name}`;
|
|
const env = { PATH: process.env.PATH, HOME: process.env.HOME, CONVEX_SYNC_WORKER_SECRET: 'inert-worker-secret', [name]: forbidden };
|
|
const child = Bun.spawn(['bun', 'scripts/run-sync-worker.ts'], { env, stdout: 'pipe', stderr: 'pipe' });
|
|
const code = await child.exited;
|
|
const output = await new Response(child.stdout).text() + await new Response(child.stderr).text();
|
|
expect(code).not.toBe(0);
|
|
expect(output).not.toContain(forbidden);
|
|
}
|
|
});
|
|
});
|