Skip to content
markpaper

src/orders/agent.test.ts

v0.3.0 · 4.1 KB

Download file
import { describe, expect, it } from 'vitest';
import type { InfoCallOptions, InfoRequest, InfoRequester } from '../transport/types.js';
import { assertAgentActive, checkAgent } from './agent.js';
import { HlOrderError } from './errors.js';

const MASTER = `0x${'Aa'.repeat(20)}`;
const AGENT = `0x${'Bb'.repeat(20)}`;
const NOW = 1_800_000_000_000;

function infoAnswer(answer: unknown | Error) {
  const calls: { body: InfoRequest; opts: InfoCallOptions | undefined }[] = [];
  const info = (async (body: InfoRequest, opts?: InfoCallOptions) => {
    calls.push({ body, opts });
    if (answer instanceof Error) throw answer;
    return answer;
  }) as InfoRequester;
  return { info, calls };
}

describe('checkAgent', () => {
  it('queries extraAgents of the master (weight 20) and matches case-insensitively', async () => {
    const { info, calls } = infoAnswer([
      { address: `0x${'cc'.repeat(20)}`, name: 'other', validUntil: null },
      { address: AGENT.toLowerCase(), name: 'bot-1', validUntil: NOW + 1000 },
    ]);
    expect(await checkAgent(info, MASTER, AGENT, { now: NOW })).toEqual({
      status: 'active',
      agent: AGENT.toLowerCase(),
      master: MASTER.toLowerCase(),
      validUntil: NOW + 1000,
      name: 'bot-1',
    });
    expect(calls).toEqual([{ body: { type: 'extraAgents', user: MASTER.toLowerCase() }, opts: { weight: 20 } }]);
  });

  it('validUntil in the past is expired; null, 0 and missing mean no expiry; numeric strings are read', async () => {
    const at = async (validUntil: unknown) =>
      (await checkAgent(infoAnswer([{ address: AGENT, name: 'x', validUntil }]).info, MASTER, AGENT, { now: NOW })).status;
    expect(await at(NOW)).toBe('expired');
    expect(await at(NOW - 1)).toBe('expired');
    expect(await at(null)).toBe('active');
    expect(await at(0)).toBe('active');
    expect(await at(undefined)).toBe('active');
    expect(await at(String(NOW + 5))).toBe('active');
    await expect(at('soon')).rejects.toMatchObject({ code: 'INVALID_RESPONSE' });
  });

  it('absent agent is not_listed (may be an unnamed agent)', async () => {
    const r = await checkAgent(infoAnswer([]).info, MASTER, AGENT);
    expect(r).toMatchObject({ status: 'not_listed', validUntil: null, name: null });
  });

  it('rejects a non-array answer and malformed addresses', async () => {
    await expect(checkAgent(infoAnswer({ agents: [] }).info, MASTER, AGENT)).rejects.toMatchObject({ code: 'INVALID_RESPONSE' });
    await expect(checkAgent(infoAnswer([]).info, '0x123', AGENT)).rejects.toMatchObject({ code: 'INVALID_ARGUMENT' });
    await expect(checkAgent(infoAnswer([]).info, MASTER, 'agent')).rejects.toMatchObject({ code: 'INVALID_ARGUMENT' });
  });
});

describe('assertAgentActive', () => {
  it('returns the check for an active agent', async () => {
    const { info } = infoAnswer([{ address: AGENT, name: 'bot', validUntil: null }]);
    expect((await assertAgentActive(info, MASTER, AGENT)).status).toBe('active');
  });

  it('throws AGENT_EXPIRED / AGENT_NOT_LISTED; allowUnlisted lets an unnamed agent through', async () => {
    await expect(assertAgentActive(infoAnswer([{ address: AGENT, validUntil: NOW - 1 }]).info, MASTER, AGENT, { now: NOW })).rejects.toMatchObject({
      code: 'AGENT_EXPIRED',
    });
    await expect(assertAgentActive(infoAnswer([]).info, MASTER, AGENT)).rejects.toMatchObject({ code: 'AGENT_NOT_LISTED' });
    expect((await assertAgentActive(infoAnswer([]).info, MASTER, AGENT, { allowUnlisted: true })).status).toBe('not_listed');
    // allowUnlisted never accepts an expired agent.
    await expect(
      assertAgentActive(infoAnswer([{ address: AGENT, validUntil: NOW - 1 }]).info, MASTER, AGENT, { now: NOW, allowUnlisted: true }),
    ).rejects.toMatchObject({ code: 'AGENT_EXPIRED' });
  });

  it('a failed read is AGENT_QUERY_FAILED with the cause: approval is unproven, fail closed', async () => {
    const cause = new Error('HL info HTTP 502');
    const err = await assertAgentActive(infoAnswer(cause).info, MASTER, AGENT).catch((e: unknown) => e);
    expect(err).toBeInstanceOf(HlOrderError);
    expect(err).toMatchObject({ code: 'AGENT_QUERY_FAILED', cause });
  });
});
All files