aiax-validate / tree / test/validate.test.ts ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { join } from 'node:path';
import { validateSite, fixtureFetch } from '../src/index.js';
import { FX, ROOT, cli, memFetch } from './helpers.ts';

const realFetch = globalThis.fetch;
test.beforeEach(() => { globalThis.fetch = (() => { throw new Error('network used'); }) as any; });
test.afterEach(() => { globalThis.fetch = realFetch; });

const run = (dir: string, url = 'https://site.example') => validateSite(url, { fetch: fixtureFetch(FX(dir)) });
const msgs = (r: any) => r.errors.map((e: any) => e.message).join('\n');

test('A1 passes on lax.kognai.ai and www.kognai.ai (recorded 21 Oct 2026): ok, score 90 or more, every section ok', async () => {
  for (const host of ['lax.kognai.ai', 'www.kognai.ai']) {
    const r = await validateSite(`https://${host}`, { fetch: fixtureFetch(FX(`live/${host}`)) });
    assert.equal(r.ok, true, msgs(r)); assert.ok(r.score >= 90, `${host} score ${r.score}`);
    assert.equal(r.manifest_url, `https://${host}/.well-known/aiax.json`);
    assert.ok(r.sections.length >= 5 && r.sections.every((s: any) => s.ok));
    const c = cli([`https://${host}`, '--fixtures', FX(`live/${host}`)]);
    assert.equal(c.code, 0, c.out);
  }
});

test('A1 a valid site scores 100; the manifest is also found at /aiax/manifest.json, and entry urls resolve against it', async () => {
  const r = await run('valid');
  assert.deepEqual([r.ok, r.score, r.errors.length, r.warnings.length], [true, 100, 0, 0]);
  const f = await run('valid-fallback');
  assert.equal(f.ok, true, msgs(f)); assert.equal(f.manifest_url, 'https://site.example/aiax/manifest.json');
  assert.equal(f.sections[0].url, 'https://site.example/aiax/sections/summarise.json');
});

test('A2 a broken manifest fails with one clear message per problem, naming its field', async () => {
  const cases: Array<[string, string, RegExp]> = [
    ['manifest-missing-field', '/updated_at', /updated_at is required/],
    ['manifest-wrong-type', '/entry_points/0/est_tokens', /entry_points\.0\.est_tokens has the wrong type: expected integer, got string/],
    ['manifest-extra-field', '/product/logo', /product\.logo is not an allowed field/],
    ['manifest-bad-entry-id', '/entry_points/1/id', /entry_points\.1\.id does not match the pattern/],
  ];
  for (const [dir, path, re] of cases) {
    const r = await run(`broken/${dir}`);
    assert.equal(r.ok, false, dir); assert.ok(r.score < 100);
    const mine = r.errors.filter((e: any) => e.url.endsWith('/.well-known/aiax.json'));
    assert.equal(mine.length, 1, `${dir}: exactly one manifest error, got ${msgs(r)}`);
    assert.equal(mine[0].path, path); assert.match(mine[0].message, re);
  }
  // several problems at once: one message each
  const m = JSON.parse(readFileSync(FX('valid/.well-known/aiax.json'), 'utf8'));
  delete m.product.name; m.aiax_version = '0.2'; m.extra = 1;
  const r = await validateSite('https://m.example', { fetch: memFetch({ '/.well-known/aiax.json': JSON.stringify(m), '/aiax/sections/summarise.json': readFileSync(FX('valid/aiax/sections/summarise.json'), 'utf8'), '/aiax/sections/trust.json': readFileSync(FX('valid/aiax/sections/trust.json'), 'utf8') }) });
  assert.deepEqual(r.errors.map((e: any) => e.path).sort(), ['/aiax_version', '/extra', '/product/name']);
  assert.equal(r.score, 100 - 3 * 15);
});

test('A2 a missing or non JSON manifest is an error', async () => {
  const r = await run('broken/manifest-missing');
  assert.equal(r.ok, false); assert.match(msgs(r), /no AIAX manifest found/); assert.equal(r.manifest_url, null);
  const j = await run('broken/manifest-not-json');
  assert.equal(j.ok, false); assert.match(msgs(j), /not JSON/);
});

test('A3 a broken section fails with clear messages: missing field, bad pricing, id mismatch, missing, not JSON', async () => {
  const cases: Array<[string, RegExp]> = [
    ['section-missing-field', /examples is required/],
    ['section-bad-pricing', /pricing\.amount has the wrong type: expected string, got number/],
    ['section-id-mismatch', /section_id "summary" differs from its entry point id "summarise"/],
    ['section-missing', /section "summarise" could not be fetched: HTTP 404/],
    ['section-not-json', /section "summarise" is not JSON/],
  ];
  for (const [dir, re] of cases) {
    const r = await run(`broken/${dir}`);
    assert.equal(r.ok, false, dir);
    const s = r.sections.find((x: any) => x.id === 'summarise');
    assert.equal(s.ok, false); assert.equal(s.errors.length, 1, `${dir}: ${msgs(r)}`);
    assert.match(s.errors[0].message, re);
    assert.equal(r.sections.find((x: any) => x.id === 'trust').ok, true, 'the other section is still checked and ok');
  }
  // bad pricing model and an over long purpose
  const sec = JSON.parse(readFileSync(FX('valid/aiax/sections/summarise.json'), 'utf8'));
  sec.pricing.model = 'monthly'; sec.purpose = 'x'.repeat(601);
  const files: any = { '/.well-known/aiax.json': readFileSync(FX('valid/.well-known/aiax.json'), 'utf8'), '/aiax/sections/summarise.json': JSON.stringify(sec), '/aiax/sections/trust.json': readFileSync(FX('valid/aiax/sections/trust.json'), 'utf8') };
  const r = await validateSite('https://s.example', { fetch: memFetch(files) });
  assert.match(msgs(r), /pricing\.model must be one of/); assert.match(msgs(r), /purpose is too long: at most 600/);
});

test('A4 --json prints the Report, and the exit codes are 0, 1 and 2', () => {
  const ok = cli(['https://site.example', '--json', '--fixtures', FX('valid')]);
  assert.equal(ok.code, 0);
  const rep = JSON.parse(ok.out);
  assert.deepEqual(Object.keys(rep).sort(), ['errors', 'manifest_url', 'ok', 'score', 'sections', 'url', 'warnings']);
  assert.equal(rep.ok, true);
  const bad = cli(['https://site.example', '--json', '--fixtures', FX('broken/section-id-mismatch')]);
  assert.equal(bad.code, 1); assert.equal(JSON.parse(bad.out).ok, false);
  const plain = cli(['https://site.example', '--fixtures', FX('broken/manifest-extra-field')]);
  assert.equal(plain.code, 1);
  assert.match(plain.out.split('\n')[0], /^error .*product\.logo is not an allowed field$/, 'one plain line per error');
  assert.equal(cli([]).code, 2);
  assert.equal(cli(['https://a.example', '--fixtures']).code, 2);
  assert.equal(cli(['https://a.example', '--bogus']).code, 2);
  assert.equal(cli(['https://a.example', 'https://b.example']).code, 2);
  assert.equal(cli(['http://[bad']).code, 2);
});

test('A4 --fixtures answers every fetch from a file and a missing file is a 404, offline', async () => {
  const f = fixtureFetch(FX('valid'));
  assert.equal((await f('https://any.example/.well-known/aiax.json')).status, 200);
  assert.equal((await f('https://any.example/nope.json')).status, 404);
  assert.equal((await f('https://any.example/../../etc/passwd')).status, 404);
});

test('A5 publishable to npm: bin aiax-validate, no runtime dependency, broken case fixtures in the repository', () => {
  const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
  assert.equal(pkg.bin['aiax-validate'], 'bin/aiax-validate.js');
  assert.deepEqual(pkg.dependencies ?? {}, {});
  assert.ok(readFileSync(join(ROOT, 'bin/aiax-validate.js'), 'utf8').startsWith('#!/usr/bin/env node'));
  const p = spawnSync('npm', ['pack', '--dry-run', '--json'], { cwd: ROOT, encoding: 'utf8' });
  assert.equal(p.status, 0, p.stderr);
  const files = JSON.parse(p.stdout)[0].files.map((f: any) => f.path);
  for (const f of ['bin/aiax-validate.js', 'src/validate.js', 'src/schema.js', 'src/schemas/manifest.schema.json', 'src/schemas/section.schema.json', 'src/index.d.ts']) assert.ok(files.includes(f), f);
  const broken = readdirSync(FX('broken'));
  for (const b of ['manifest-missing-field', 'manifest-wrong-type', 'manifest-extra-field', 'manifest-bad-entry-id', 'section-missing-field', 'section-bad-pricing', 'section-id-mismatch', 'section-missing', 'section-not-json']) assert.ok(broken.includes(b), b);
});

test('A5 the schemas are the AIAX 0.1 schemas, unchanged', () => {
  for (const f of ['manifest.schema.json', 'section.schema.json']) {
    const s = JSON.parse(readFileSync(join(ROOT, 'src/schemas', f), 'utf8'));
    assert.match(s.$id, new RegExp(`Godman-s/aiax/main/schemas/${f}$`));
    assert.deepEqual(s.properties.aiax_version.enum, ['0.1']);
  }
  assert.ok(existsSync(join(ROOT, 'src/schemas/NOTICE.md')));
});

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