Skip to content

Commit 6999f1c

Browse files
committed
test(#246): replay harness foundation — record real API responses, replay through the real pipeline
Stands up deterministic integration tests with no live API [skip-release]: - test/cassettes/record.mjs — records real CWA API responses (route → manifest → all resource IRIs) into a committed cassette; drives the dev API (self-signed cert accepted). First cassette: topic-1-nested (home, /topic-1 redirect, chapter-one/two nested siblings, form, real 404 sub-resources, link headers). - ReplayCwaFetch — serves cassette responses via fetch.raw, throwing ofetch-shaped errors for non-2xx, with CONTROLLABLE TIMING (manual hold/release) so tests can interleave navigations and reproduce timing races deterministically. - Boot harness — assembles the real Fetcher + FetchStatusManager + Pinia stores + Resources against the replay, mocking only useError/clearError + nuxt/app. - First integration test: a nested navigation loads parent + child correctly from recorded responses through the whole pipeline. - vitest.config: also run test/**/*.spec.ts. Next: manual-timing race tests (switch-before-load / stuck), then re-apply #257 and reproduce the regression against this harness.
1 parent 91ed679 commit 6999f1c

7 files changed

Lines changed: 3405 additions & 1 deletion

File tree

test/cassettes/record.mjs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env node
2+
/**
3+
* #246 — Record real CWA API responses into a replay cassette for deterministic integration tests.
4+
*
5+
* Usage (dev API must be running; self-signed cert is accepted):
6+
* node test/cassettes/record.mjs <cassette-name> <routePath> [routePath...]
7+
*
8+
* e.g. node test/cassettes/record.mjs topic-1-nested /topic-1 /topic-1/chapter-one /topic-1/chapter-two /
9+
*
10+
* For each route path it records the route resource, its manifest, and every resource IRI in the
11+
* manifest tree (deduped). Keyed by the exact request path the fetcher issues (with the /_api prefix).
12+
*/
13+
import { writeFileSync, mkdirSync } from 'node:fs'
14+
import { dirname } from 'node:path'
15+
import { fileURLToPath } from 'node:url'
16+
17+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
18+
19+
const ORIGIN = 'https://localhost'
20+
const PREFIX = '/_api'
21+
const HERE = dirname(fileURLToPath(import.meta.url))
22+
23+
const cassetteName = process.argv[2]
24+
const routePaths = process.argv.slice(3)
25+
if (!cassetteName || !routePaths.length) {
26+
console.error('Usage: node test/cassettes/record.mjs <cassette-name> <routePath> [routePath...]')
27+
process.exit(1)
28+
}
29+
30+
const entries = new Map() // cleanPath -> entry
31+
32+
async function record(path) {
33+
const clean = path.split('?')[0]
34+
if (entries.has(clean)) {
35+
return entries.get(clean)
36+
}
37+
let status = 0
38+
let body = null
39+
let link = null
40+
try {
41+
const res = await fetch(`${ORIGIN}${clean}`, { headers: { accept: 'application/ld+json,application/json' } })
42+
status = res.status
43+
link = res.headers.get('link')
44+
try {
45+
body = await res.json()
46+
}
47+
catch {
48+
body = null
49+
}
50+
}
51+
catch (e) {
52+
console.error(` ! request failed for ${clean}: ${e.message}`)
53+
}
54+
const entry = { method: 'GET', path: clean, status, body, headers: link ? { link } : {} }
55+
entries.set(clean, entry)
56+
return entry
57+
}
58+
59+
function flattenTree(nodes) {
60+
const iris = []
61+
const walk = (n) => {
62+
if (n?.iri) {
63+
iris.push(n.iri)
64+
}
65+
;(n?.children || []).forEach(walk)
66+
}
67+
;(nodes || []).forEach(walk)
68+
return iris
69+
}
70+
71+
for (const rp of routePaths) {
72+
console.log(`recording route ${rp} ...`)
73+
await record(`${PREFIX}/_/routes/${rp}`)
74+
const manifest = await record(`${PREFIX}/_/resource_manifest/${rp}`)
75+
const iris = flattenTree(manifest.body?.resource_iris)
76+
for (const iri of iris) {
77+
await record(iri)
78+
}
79+
}
80+
81+
mkdirSync(HERE, { recursive: true })
82+
const outPath = `${HERE}/${cassetteName}.json`
83+
writeFileSync(outPath, JSON.stringify({ name: cassetteName, recordedAt: '', entries: [...entries.values()] }, null, 2))
84+
console.log(`\nRecorded ${entries.size} entries -> test/cassettes/${cassetteName}.json`)

0 commit comments

Comments
 (0)