lax / tree / test/jobs.test.ts ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
/**
 * Tests for the LAX jobs client. Each test is named after the acceptance item it proves.
 * Offline: every test passes its own fetch and sleep. A7 reads the live registry only when LAX_LIVE=1.
 * Run: npm run test:jobs
 */
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { createJobsClient, LaxJobsError } from '../src/index.js';
import type { FetchInitLike, FetchResponseLike, SubmissionInput } from '../src/index.js';

const BASE = 'https://lax.kognai.ai/registry';
const JOB = { id: 'job/1 a', title: 'A job', track: 'godman-protocols', status: 'open', bounty_usdc: 200 };
const SUBMISSION = { id: 's1', job_id: 'job-1', status: 'received', created_at: '2026-11-01T00:00:00Z' };
const GOOD: SubmissionInput = {
  builder: 'Bounty Hunter',
  contact: 'ops@example.com',
  repo_or_patch_url: 'https://github.com/example/lax/commit/0123456789abcdef0123456789abcdef01234567',
  payout_address: '0x3c1A1E01e790Cd326495631B3b38Bd7068F08d6F',
  agent_did: 'did:pkh:eip155:8453:0x3c1A1E01e790Cd326495631B3b38Bd7068F08d6F',
  notes: 'M1',
};

type Step = { status: number; body?: unknown; raw?: string; headers?: Record<string, string> } | { throws: string };
interface Call { url: string; init: FetchInitLike }

function fakeFetch(steps: Step[]) {
  const calls: Call[] = [];
  const fetch = async (url: string, init: FetchInitLike): Promise<FetchResponseLike> => {
    calls.push({ url, init });
    const step = steps[Math.min(calls.length - 1, steps.length - 1)];
    if (!step) throw new Error('no step');
    if ('throws' in step) throw new TypeError(step.throws);
    const h = Object.fromEntries(Object.entries(step.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v]));
    return {
      status: step.status,
      headers: { get: (n: string) => h[n.toLowerCase()] ?? null },
      text: async () => (step.raw !== undefined ? step.raw : step.body === undefined ? '' : JSON.stringify(step.body)),
    };
  };
  return { fetch, calls };
}

function fakeSleep() {
  const waits: number[] = [];
  return { sleep: async (ms: number) => { waits.push(ms); }, waits };
}

async function rejects(p: Promise<unknown>, check: Partial<{ status: number; code: string; message: string }>) {
  try {
    await p;
  } catch (e) {
    assert.ok(e instanceof LaxJobsError, 'throws LaxJobsError');
    assert.ok(e instanceof Error);
    for (const [k, v] of Object.entries(check)) assert.equal((e as unknown as Record<string, unknown>)[k], v, k);
    return e as LaxJobsError;
  }
  assert.fail('expected a LaxJobsError');
}

test('A1 listJobs sends GET /jobs with only the given parameters and returns {jobs, count}', async () => {
  const f = fakeFetch([{ status: 200, body: { jobs: [JOB], count: 1 } }]);
  const c = createJobsClient({ fetch: f.fetch, sleep: fakeSleep().sleep });
  assert.deepEqual(await c.listJobs(), { jobs: [JOB], count: 1 });
  assert.equal(f.calls[0]!.url, `${BASE}/jobs`);
  assert.equal(f.calls[0]!.init.method, 'GET');
  await c.listJobs({ track: 'genesis-tools', limit: 100, offset: 0 });
  assert.equal(f.calls[1]!.url, `${BASE}/jobs?track=genesis-tools&limit=100&offset=0`);
  await c.listJobs({ limit: 1 });
  assert.equal(f.calls[2]!.url, `${BASE}/jobs?limit=1`);
});

test('A1 listJobs refuses a limit outside 1 to 100 before any request', async () => {
  const f = fakeFetch([{ status: 200, body: { jobs: [], count: 0 } }]);
  const c = createJobsClient({ fetch: f.fetch });
  await rejects(c.listJobs({ limit: 0 }), { code: 'invalid_input', status: 0 });
  await rejects(c.listJobs({ limit: 101 }), { code: 'invalid_input' });
  assert.equal(f.calls.length, 0);
});

test('A1 getJob sends GET /jobs/<id> with the id URL encoded and returns the job out of {job}', async () => {
  const f = fakeFetch([{ status: 200, body: { job: JOB } }]);
  const c = createJobsClient({ fetch: f.fetch, baseUrl: 'https://example.test/registry/' });
  assert.deepEqual(await c.getJob('job/1 a'), JOB);
  assert.equal(f.calls[0]!.url, 'https://example.test/registry/jobs/job%2F1%20a');
  assert.equal(f.calls[0]!.init.method, 'GET');
});

test('A1 submit POSTs JSON to /jobs/<id>/submissions and returns the submission out of {submission}', async () => {
  const f = fakeFetch([{ status: 201, body: { submission: SUBMISSION } }]);
  const c = createJobsClient({ fetch: f.fetch });
  assert.deepEqual(await c.submit('job-1', GOOD), SUBMISSION);
  const call = f.calls[0]!;
  assert.equal(call.url, `${BASE}/jobs/job-1/submissions`);
  assert.equal(call.init.method, 'POST');
  assert.equal(call.init.headers['content-type'], 'application/json');
  assert.deepEqual(JSON.parse(call.init.body!), GOOD);
});

test('A1 a 2xx answer that is not JSON or lacks its key throws invalid_response', async () => {
  const c1 = createJobsClient({ fetch: fakeFetch([{ status: 200, raw: '<html>' }]).fetch });
  await rejects(c1.getJob('x'), { code: 'invalid_response', status: 200 });
  const c2 = createJobsClient({ fetch: fakeFetch([{ status: 200, body: { nope: 1 } }]).fetch });
  await rejects(c2.getJob('x'), { code: 'invalid_response' });
  await rejects(c2.listJobs(), { code: 'invalid_response' });
});

for (const [status, code] of [[400, 'bad_request'], [404, 'not_found'], [409, 'conflict']] as const) {
  test(`A2 a ${status} answer throws LaxJobsError ${code} with the registry message after a single request`, async () => {
    for (const run of ['list', 'get', 'submit'] as const) {
      const f = fakeFetch([{ status, body: { error: `registry says ${status}` } }]);
      const s = fakeSleep();
      const c = createJobsClient({ fetch: f.fetch, sleep: s.sleep, retries: 5 });
      const p = run === 'list' ? c.listJobs() : run === 'get' ? c.getJob('j') : c.submit('j', GOOD);
      await rejects(p, { status, code, message: `registry says ${status}` });
      assert.equal(f.calls.length, 1, `${run}: one request`);
      assert.equal(s.waits.length, 0);
    }
  });
}

test('A3 a 429 is retried after its Retry-After and then succeeds', async () => {
  const f = fakeFetch([
    { status: 429, body: { error: 'slow down' }, headers: { 'Retry-After': '3' } },
    { status: 200, body: { job: JOB } },
  ]);
  const s = fakeSleep();
  const c = createJobsClient({ fetch: f.fetch, sleep: s.sleep });
  assert.deepEqual(await c.getJob('x'), JOB);
  assert.deepEqual(s.waits, [3000]);
  assert.equal(f.calls.length, 2);
});

test('A3 Retry-After is capped at 30 seconds, and without it the wait starts at 500 ms and doubles', async () => {
  const s = fakeSleep();
  const c = createJobsClient({
    fetch: fakeFetch([
      { status: 429, headers: { 'retry-after': '120' } },
      { status: 429 },
      { status: 429 },
      { status: 201, body: { submission: SUBMISSION } },
    ]).fetch,
    sleep: s.sleep,
    retries: 3,
  });
  assert.deepEqual(await c.submit('j', GOOD), SUBMISSION);
  assert.deepEqual(s.waits, [30000, 1000, 2000]);
});

test('A3 a 429 that never ends throws rate_limited after the last retry', async () => {
  for (const run of ['list', 'submit'] as const) {
    const f = fakeFetch([{ status: 429, body: { error: 'too many submissions' }, headers: { 'retry-after': '1' } }]);
    const s = fakeSleep();
    const c = createJobsClient({ fetch: f.fetch, sleep: s.sleep });
    const p = run === 'list' ? c.listJobs() : c.submit('j', GOOD);
    await rejects(p, { status: 429, code: 'rate_limited', message: 'too many submissions' });
    assert.equal(f.calls.length, 3, `${run}: 1 + 2 retries`);
    assert.deepEqual(s.waits, [1000, 1000]);
  }
});

test('A4 a 5xx on a read is retried with backoff and then throws server', async () => {
  const f = fakeFetch([{ status: 503, body: { error: 'down' } }]);
  const s = fakeSleep();
  const c = createJobsClient({ fetch: f.fetch, sleep: s.sleep });
  await rejects(c.listJobs(), { status: 503, code: 'server', message: 'down' });
  assert.equal(f.calls.length, 3);
  assert.deepEqual(s.waits, [500, 1000]);
});

test('A4 a 5xx then success on a read returns the answer', async () => {
  const c = createJobsClient({
    fetch: fakeFetch([{ status: 500 }, { status: 200, body: { job: JOB } }]).fetch,
    sleep: fakeSleep().sleep,
  });
  assert.deepEqual(await c.getJob('x'), JOB);
});

test('A4 a network error on a read is retried and then throws network with status 0', async () => {
  const f = fakeFetch([{ throws: 'fetch failed' }]);
  const s = fakeSleep();
  const c = createJobsClient({ fetch: f.fetch, sleep: s.sleep, retries: 1 });
  await rejects(c.getJob('x'), { status: 0, code: 'network', message: 'fetch failed' });
  assert.equal(f.calls.length, 2);
  assert.deepEqual(s.waits, [500]);
});

test('A4 submit is not retried on a 5xx or a network error', async () => {
  const f1 = fakeFetch([{ status: 502, body: { error: 'bad gateway' } }]);
  const c1 = createJobsClient({ fetch: f1.fetch, sleep: fakeSleep().sleep });
  await rejects(c1.submit('j', GOOD), { status: 502, code: 'server' });
  assert.equal(f1.calls.length, 1);
  const f2 = fakeFetch([{ throws: 'socket hang up' }]);
  const c2 = createJobsClient({ fetch: f2.fetch, sleep: fakeSleep().sleep });
  await rejects(c2.submit('j', GOOD), { status: 0, code: 'network' });
  assert.equal(f2.calls.length, 1);
});

test('A4 a request that passes timeoutMs is aborted and reported as network', async () => {
  const hanging = (_url: string, init: FetchInitLike) =>
    new Promise<FetchResponseLike>((_res, rej) => {
      (init.signal as { addEventListener(t: string, f: () => void): void }).addEventListener('abort', () => rej(new Error('aborted')));
    });
  const c = createJobsClient({ fetch: hanging, sleep: fakeSleep().sleep, retries: 0, timeoutMs: 20 });
  const e = await rejects(c.getJob('x'), { status: 0, code: 'network' });
  assert.match(e.message, /timed out/);
});

test('A5 submit refuses bad input with invalid_input before any request', async () => {
  const f = fakeFetch([{ status: 201, body: { submission: SUBMISSION } }]);
  const c = createJobsClient({ fetch: f.fetch });
  const { agent_did: _omit, ...noDid } = GOOD;
  const cases: unknown[] = [
    noDid,
    { ...GOOD, agent_did: '' },
    { ...GOOD, agent_did: 'agent-42' },
    { ...GOOD, payout_address: '0x123' },
    { ...GOOD, payout_address: '3c1A1E01e790Cd326495631B3b38Bd7068F08d6F' },
    { ...GOOD, payout_address: '0xZZ1A1E01e790Cd326495631B3b38Bd7068F08d6F' },
    { ...GOOD, repo_or_patch_url: 'http://github.com/example/lax' },
    { ...GOOD, repo_or_patch_url: 'git@github.com:example/lax.git' },
    { ...GOOD, repo_or_patch_url: 'https://' },
  ];
  for (const input of cases) {
    await rejects(c.submit('j', input as SubmissionInput), { code: 'invalid_input', status: 0 });
  }
  assert.equal(f.calls.length, 0);
});

test('A6 no new runtime dependency and no console output from the client', async () => {
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
  assert.deepEqual(pkg.dependencies ?? {}, {});
  const src = readFileSync(new URL('../src/jobs.ts', import.meta.url), 'utf8');
  assert.doesNotMatch(src, /\bconsole\./);
  assert.doesNotMatch(src, /^\s*import\s.+from\s+['"](?!\.)/m, 'no package imports');
  const methods = ['log', 'info', 'warn', 'error', 'debug'] as const;
  const saved = methods.map((m) => console[m]);
  let printed = 0;
  for (const m of methods) console[m] = () => { printed++; };
  try {
    const s = fakeSleep();
    const c = createJobsClient({ fetch: fakeFetch([{ status: 500 }, { throws: 'x' }, { status: 404, body: { error: 'nf' } }]).fetch, sleep: s.sleep });
    await c.getJob('x').catch(() => undefined);
    await c.submit('j', { ...GOOD, payout_address: 'bad' }).catch(() => undefined);
  } finally {
    methods.forEach((m, i) => { console[m] = saved[i]!; });
  }
  assert.equal(printed, 0);
});

test('A7 live: GET /jobs on the real registry (runs only with LAX_LIVE=1)', { skip: process.env.LAX_LIVE !== '1' && 'set LAX_LIVE=1 to run' }, async () => {
  const c = createJobsClient();
  const r = await c.listJobs({ limit: 5 });
  assert.ok(Array.isArray(r.jobs));
  assert.equal(typeof r.count, 'number');
  assert.ok(r.jobs.length <= 5);
  for (const j of r.jobs) assert.equal(typeof j.id, 'string');
});

agentgate-source: code mirror on Cloudflare Pages. All repos