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