ec27228a8e
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
81 lines
3.4 KiB
TypeScript
81 lines
3.4 KiB
TypeScript
/**
|
|
* Spec-snapshot test.
|
|
*
|
|
* Locks down the high-level shape of the v1 endpoint registry so an
|
|
* unintentional Zod-schema change can't ship a silent API break. CI fails
|
|
* if any of the following invariants drift unexpectedly:
|
|
*
|
|
* - Endpoint count
|
|
* - Endpoint key set (method + path tuples)
|
|
* - Set of distinct scopes referenced across all endpoints
|
|
*
|
|
* When you intentionally add or remove an endpoint, run the test once
|
|
* locally with `--update` to refresh the snapshot, review the diff, and
|
|
* commit the new snapshot alongside the route change. The diff itself
|
|
* becomes a self-describing API changelog entry.
|
|
*
|
|
* Why this lives here and not in tests/: the snapshot must be loaded
|
|
* relative to a path the load-routes side-effect import resolves from.
|
|
* Co-locating with the registry keeps the dependency cycle minimal.
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
import { listEndpoints } from '../registry'
|
|
// Side-effect import: every route file's registerEndpoint() runs at
|
|
// module load time and populates the shared ENDPOINTS map.
|
|
import '../load-routes'
|
|
|
|
describe('v1 spec snapshot', () => {
|
|
const endpoints = listEndpoints()
|
|
|
|
it('matches the recorded endpoint count', () => {
|
|
// Update intentionally when adding/removing endpoints. The count is
|
|
// the cheapest first-line check: if it changes unexpectedly, CI
|
|
// surfaces the surprise before reviewers have to spot it in the diff.
|
|
expect(endpoints.length).toMatchSnapshot('endpoint-count')
|
|
})
|
|
|
|
it('matches the recorded endpoint key set', () => {
|
|
const keys = endpoints
|
|
.map((e) => `${e.method} ${e.path}`)
|
|
.sort()
|
|
expect(keys).toMatchSnapshot('endpoint-keys')
|
|
})
|
|
|
|
it('matches the recorded scope catalogue', () => {
|
|
const scopes = Array.from(
|
|
new Set(endpoints.map((e) => e.scope ?? 'public')),
|
|
).sort()
|
|
expect(scopes).toMatchSnapshot('endpoint-scopes')
|
|
})
|
|
|
|
it('every endpoint has the agent-facing metadata that the docs depend on', () => {
|
|
// The /docs/api/reference pages and /llms-full.txt aggregator both
|
|
// assume every endpoint registers complete metadata. A registerEndpoint
|
|
// call that omits any of these fields would render a page with empty
|
|
// sections: surface the omission here instead.
|
|
for (const ep of endpoints) {
|
|
const ctx = `${ep.method} ${ep.path}`
|
|
expect(ep.summary, `${ctx}: missing summary`).toBeTruthy()
|
|
expect(ep.description, `${ctx}: missing description`).toBeTruthy()
|
|
expect(ep.useWhen, `${ctx}: missing useWhen`).toBeTruthy()
|
|
expect(ep.doNotUseFor, `${ctx}: missing doNotUseFor`).toBeTruthy()
|
|
expect(Array.isArray(ep.pitfalls), `${ctx}: pitfalls must be an array`).toBe(true)
|
|
expect(ep.example, `${ctx}: missing example`).toBeTruthy()
|
|
expect(ep.example.response, `${ctx}: example.response is required`).toBeTruthy()
|
|
|
|
// Defense-in-depth: every endpoint MUST explicitly declare its
|
|
// scope (or the literal sentinel `null` for genuinely public
|
|
// endpoints: e.g. /api/v1/health). `undefined` means the
|
|
// registerEndpoint call silently dropped the field, which would
|
|
// make the wrapper treat the route as unauthenticated. CC6.3:
|
|
// surfacing the omission in CI prevents accidental public
|
|
// exposure of new endpoints.
|
|
expect(
|
|
ep.scope !== undefined,
|
|
`${ctx}: scope must be explicitly declared (use null for genuinely public endpoints)`,
|
|
).toBe(true)
|
|
}
|
|
})
|
|
})
|