// What knotless-serve answers over XRPC, against the lexicons that say // what it answers: each answer the screenshots recorded (shots/ // fixtures.json), as `just shots-record` takes them from the real server, // or, given a directory, each one knotless-serve's tests received, as // harness/cov.mjs gathers them there (lines of `{path, body}`). A query's // output must be valid for its lexicon, a failure must be XRPC's // `{error, message}`, and every parameter asked must be one it declares. // // node harness/answers.mjs [dir] // // @atproto/lexicon is the harness's, or, where its packages are not // installed, one NODE_PATH names. import { readdirSync, readFileSync } from 'node:fs' import { createRequire } from 'node:module' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const { Lexicons } = createRequire(import.meta.url)('@atproto/lexicon') const files = (d) => readdirSync(d, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? files(join(d, e.name)) : e.name.endsWith('.json') ? [join(d, e.name)] : [], ) const docs = files(join(root, 'lexicons')).map((f) => JSON.parse(readFileSync(f, 'utf8'))) const lexicons = new Lexicons(docs) // A record in a space is named in full, as atproto's permissioned data // proposal (0016) addresses it: // at:///space/////. The // harness's @atproto/lexicon knows only the public grammar of an at-uri, // so such a URI is held to it as its public form, its last three segments; // knotless's own parser (crates/knotless/src/aturi.rs) holds the rest. const SPACED = /^at:\/\/did:[^/]+\/space\/[^/]+\/[^/]+\/(did:[^/]+\/[^/]+\/[^/]+)$/ const publicForm = (v) => typeof v === 'string' ? v.replace(SPACED, 'at://$1') : Array.isArray(v) ? v.map(publicForm) : v && typeof v === 'object' ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, publicForm(x)])) : v const [dir] = process.argv.slice(2) // Each answer as [path, {status, headers, body}], the body as sent. const answers = dir ? readdirSync(dir).flatMap((f) => readFileSync(join(dir, f), 'utf8') .split('\n') .filter(Boolean) .map((l) => JSON.parse(l)) .map(({ path, body }) => [ path, { status: 200, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }, ]), ) : Object.entries( JSON.parse(readFileSync(join(root, 'harness/shots/fixtures.json'), 'utf8')).answers, ) const problems = [] // A path asked again and again is said once. const said = new Set() let checked = 0 for (const [path, answer] of answers) { const url = new URL(path, 'http://knotless') const nsid = url.pathname.match(/^\/xrpc\/(.+)$/)?.[1] if (!nsid) continue const say = (what) => { const p = `${url.pathname}: ${what}` if (!said.has(p)) problems.push(`${path}: ${what}`) said.add(p) } const def = lexicons.getDef(nsid) if (!def) { say('no lexicon names this method') continue } const declared = Object.keys(def.parameters?.properties ?? {}) for (const name of url.searchParams.keys()) if (!declared.includes(name)) say(`${name} is no parameter of ${nsid}`) checked++ if (answer.headers['content-type'] !== 'application/json') continue const body = JSON.parse(answer.body) if (answer.status !== 200) { if (typeof body.error !== 'string' || typeof body.message !== 'string') say('a failure is {error, message}') continue } try { lexicons.assertValidXrpcOutput(nsid, publicForm(body)) } catch (e) { say(e.message) } } for (const p of problems) console.log(p) if (problems.length) process.exit(1) console.log( `answers: ${checked} ${dir ? 'XRPC answers the tests received' : 'recorded XRPC answers'} are as their lexicons say`, )