| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- /** Format reference coverage follows the writer version and retains complete historical schemas. */
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
- import { tmpdir } from 'node:os'
- import { join } from 'node:path'
- import { dump } from 'js-yaml'
- import { afterEach, describe, expect, it } from 'vitest'
- import { loadPersistenceFormats, runPersistenceFormats } from './persistence-formats.ts'
- import { parseHistoricalPersistenceSnapshot } from './persistence-changes.ts'
- import { canonicalizeSchema, schemaDigest } from './persistence-schema-model.ts'
- import type { PersistenceRoot, PersistenceSchemaInventory, PersistenceType, SchemaNode } from './persistence-schema-model.ts'
- const temporary: string[] = []
- afterEach(() => { for (const root of temporary.splice(0)) rmSync(root, { recursive: true, force: true }) })
- function rootSchema(metadata: Omit<PersistenceRoot, 'schema' | 'digest'>, nodes: readonly SchemaNode[]): PersistenceRoot {
- const schema = canonicalizeSchema(nodes, 0)
- return { ...metadata, schema, digest: schemaDigest(schema) }
- }
- function completeInventory(roots: readonly PersistenceRoot[]): PersistenceSchemaInventory {
- const types = new Map<string, PersistenceType>()
- for (const root of roots) {
- root.schema.nodes.forEach((_, index) => {
- const schema = canonicalizeSchema(root.schema.nodes, index)
- const digest = schemaDigest(schema)
- types.set(digest, { digest, schema, names: [], sources: [] })
- })
- }
- return { formatVersion: 1, roots, types: [...types.values()] }
- }
- function header(key: string, version: number | 'number', optional = false): PersistenceRoot {
- return rootSchema({ key, kind: 'header' }, [
- { kind: 'object', indices: [], properties: [{ name: 'version', type: 1, optional }] },
- version === 'number' ? { kind: 'primitive', type: 'number' } : { kind: 'literal', value: version },
- ])
- }
- function inventory(version: number): PersistenceSchemaInventory {
- return completeInventory([
- header('SessionHeader', version), header('JsonlHeaderLine', 'number'),
- rootSchema({ key: 'SessionEventEnvelope', kind: 'envelope' }, [{ kind: 'object', indices: [], properties: [] }]),
- rootSchema({ key: 'event:example/value', kind: 'event', event: 'example/value', surface: false }, [
- { kind: 'object', indices: [], properties: [{ name: 'type', type: 1, optional: false }, { name: 'data', type: 2, optional: false }] },
- { kind: 'literal', value: 'example/value' }, { kind: 'primitive', type: 'string' },
- ]),
- ])
- }
- function write(root: string, path: string, content: string): void {
- writeFileSync(join(root, path), content)
- }
- function schemaPath(version: number, currentVersion: number): string {
- return version === currentVersion ? 'docs/persistence-schema.json' : `docs/persistence-changes/historical-formats/v${version}.schema.json`
- }
- function replaceSnapshot(root: string, version: number, currentVersion: number, snapshot: PersistenceSchemaInventory): void {
- write(root, schemaPath(version, currentVersion), JSON.stringify(snapshot))
- }
- function saveReference(root: string, version: number, currentVersion: number, snapshot = inventory(version)): void {
- const current = version === currentVersion
- const filename = current ? 'persistence-catalog' : `persistence-changes/historical-formats/v${version}`
- const schemaName = current ? 'persistence-schema.json' : `v${version}.schema.json`
- const record = {
- schemaVersion: 1, sessionFormatVersion: version,
- source: version === 1 ? { pullRequest: 3349 } : { tag: 'dsh-v0.1.0-rc.5' },
- roots: Object.fromEntries(snapshot.roots.map(root => [root.key, root.digest])),
- }
- const lines = [
- ...current ? [] : ['---', 'kind: persistence-format', '---', '', '```yaml persistence-format', dump(record).trimEnd(), '```', ''],
- `# Session format v${version}`, '', `[Inventory](${schemaName})`, '', '| Root | Kind | SHA-256 |', '|---|---|---|',
- ...snapshot.roots.map(root => `| \`${root.key}\` | ${root.kind} | \`${root.digest}\` |`), '',
- ]
- for (const suffix of ['.md', '.zh.md']) write(root, `docs/${filename}${suffix}`, lines.join('\n'))
- replaceSnapshot(root, version, currentVersion, snapshot)
- }
- function fixture(currentVersion = 3): string {
- const root = mkdtempSync(join(tmpdir(), 'dsh-persistence-formats-'))
- temporary.push(root)
- mkdirSync(join(root, 'packages/core/session/src'), { recursive: true })
- mkdirSync(join(root, 'docs/persistence-changes/historical-formats'), { recursive: true })
- write(root, 'packages/core/session/src/types.ts', `export const SESSION_FORMAT_VERSION = ${currentVersion} as const\n`)
- for (let version = 0; version <= currentVersion; version += 1) saveReference(root, version, currentVersion)
- return root
- }
- function edit(root: string, path: string, from: string, to: string): void {
- const source = readFileSync(join(root, path), 'utf8')
- expect(source).toContain(from)
- write(root, path, source.replace(from, to))
- }
- function editPair(root: string, version: number, from: string, to: string): void {
- for (const suffix of ['.md', '.zh.md']) edit(root, `docs/persistence-changes/historical-formats/v${version}${suffix}`, from, to)
- }
- function prepareFacts(root: string, currentVersion = 3): void {
- for (const suffix of ['.md', '.zh.md']) {
- for (let version = 0; version < currentVersion; version += 1) {
- const path = `docs/persistence-changes/historical-formats/v${version}${suffix}`
- const switcher = suffix === '.md' ? `English | [中文](v${version}.zh.md)` : `[English](v${version}.md) | 中文`
- write(root, path, readFileSync(join(root, path), 'utf8') + `\n${switcher}\n\n<!-- persistence-format-schema:start -->\n\nPending schemas.\n\n<!-- persistence-format-schema:end -->\n`)
- }
- const switcher = suffix === '.md' ? 'English | [中文](README.zh.md)' : '[English](README.md) | 中文'
- write(root, `docs/persistence-changes/historical-formats/README${suffix}`, `# Session formats\n\n${switcher}\n\nAuthored context.\n\n<!-- persistence-format-index:start -->\n\nPending formats.\n\n<!-- persistence-format-index:end -->\n`)
- }
- }
- describe('archive current persistence format', () => {
- it('keeps every reachable type and drops extractor-retained types and source line numbers', () => {
- const root = fixture()
- const original = inventory(3)
- const sources = [
- 'packages/example/src/types.ts:12', 'packages/other/src/types.ts:8:2', 'packages/example/src/types.ts:34:5',
- 'packages/plain/src/types.ts', 'packages/fragment/src/types.ts#L8', 'packages/fragment/src/types.ts#L8-L12',
- ]
- const types = original.types.map(type => ({ ...type, names: ['Example'], sources }))
- const schema = canonicalizeSchema([{ kind: 'literal', value: 'unreferenced' }], 0)
- replaceSnapshot(root, 3, 3, { ...original, types: [...types, { schema, digest: schemaDigest(schema), names: [], sources: [] }] })
- const before = readFileSync(join(root, 'docs/persistence-schema.json'), 'utf8')
- expect(runPersistenceFormats(['--archive', '3'], root)).toBe('Archived Session format v3 to docs/persistence-changes/historical-formats/v3.schema.json.')
- const serialized = readFileSync(join(root, 'docs/persistence-changes/historical-formats/v3.schema.json'), 'utf8')
- const archived = parseHistoricalPersistenceSnapshot(JSON.parse(serialized))
- expect(archived).toEqual({
- ...original,
- types: types.map(type => ({ ...type, sources: [
- 'packages/example/src/types.ts', 'packages/other/src/types.ts', 'packages/plain/src/types.ts', 'packages/fragment/src/types.ts',
- ] })),
- })
- expect(serialized).toBe(JSON.stringify(archived, null, 2) + '\n')
- expect(readFileSync(join(root, 'docs/persistence-schema.json'), 'utf8')).toBe(before)
- expect(existsSync(join(root, 'docs/persistence-changes/historical-formats/v3.md'))).toBe(false)
- saveReference(root, 3, 4, archived)
- write(root, 'packages/core/session/src/types.ts', 'export const SESSION_FORMAT_VERSION = 4 as const\n')
- saveReference(root, 4, 4)
- expect(loadPersistenceFormats(root).entries[3]?.inventory).toEqual(archived)
- })
- it('creates the first archive directory and honors --root', () => {
- const root = fixture(0)
- rmSync(join(root, 'docs/persistence-changes/historical-formats'), { recursive: true })
- expect(runPersistenceFormats(['--root', root, '--archive', '0'])).toBe('Archived Session format v0 to docs/persistence-changes/historical-formats/v0.schema.json.')
- expect(JSON.parse(readFileSync(join(root, 'docs/persistence-changes/historical-formats/v0.schema.json'), 'utf8'))).toEqual(inventory(0))
- })
- it.each(['-1', '1.5', '01', '3junk', '9007199254740992'])('rejects an invalid archive version %s', (version) => {
- const root = fixture()
- expect(() => runPersistenceFormats([`--archive=${version}`], root)).toThrow('--archive must be a non-negative safe integer')
- expect(existsSync(join(root, 'docs/persistence-changes/historical-formats/v3.schema.json'))).toBe(false)
- })
- it.each(['2', '4'])('rejects archive v%s when the current writer is v3', (version) => {
- const root = fixture()
- expect(() => runPersistenceFormats(['--archive', version], root)).toThrow(`Cannot archive v${version}: current writer is v3`)
- expect(existsSync(join(root, 'docs/persistence-changes/historical-formats/v3.schema.json'))).toBe(false)
- })
- it('refuses to overwrite an existing archive', () => {
- const root = fixture()
- write(root, 'docs/persistence-changes/historical-formats/v3.schema.json', 'Retained history.\n')
- expect(() => runPersistenceFormats(['--archive', '3'], root)).toThrow('docs/persistence-changes/historical-formats/v3.schema.json already exists')
- expect(readFileSync(join(root, 'docs/persistence-changes/historical-formats/v3.schema.json'), 'utf8')).toBe('Retained history.\n')
- })
- it('rejects --archive with --write before writing', () => {
- const root = fixture()
- expect(() => runPersistenceFormats(['--archive', '3', '--write'], root)).toThrow('--archive and --write cannot be combined')
- expect(existsSync(join(root, 'docs/persistence-changes/historical-formats/v3.schema.json'))).toBe(false)
- })
- it.each(['invalid-json', 'digest-drift', 'missing-type', 'missing-source-metadata', 'outdated-header'])('rejects %s before creating an archive', (variant) => {
- const root = fixture()
- const original = inventory(3)
- if (variant === 'invalid-json') write(root, 'docs/persistence-schema.json', '{')
- else if (variant === 'digest-drift') replaceSnapshot(root, 3, 3, { ...original, roots: [{ ...original.roots[0]!, digest: '0'.repeat(64) }, ...original.roots.slice(1)] })
- else if (variant === 'missing-type') replaceSnapshot(root, 3, 3, { ...original, types: original.types.slice(1) })
- else if (variant === 'missing-source-metadata') edit(root, 'docs/persistence-schema.json', ',"sources":[]', '')
- else replaceSnapshot(root, 3, 3, inventory(2))
- expect(() => runPersistenceFormats(['--archive', '3'], root)).toThrow(variant === 'invalid-json' ? SyntaxError
- : variant === 'digest-drift' ? 'schema digest mismatch' : variant === 'missing-type' ? 'cover every reachable type'
- : variant === 'missing-source-metadata' ? 'missing field sources' : 'SessionHeader.version must match')
- expect(existsSync(join(root, 'docs/persistence-changes/historical-formats/v3.schema.json'))).toBe(false)
- })
- })
- describe('complete persistence format references', () => {
- it.each(['.md', '.zh.md'])('rejects source coordinates in authored format evidence in %s', (suffix) => {
- const root = fixture()
- const path = `docs/persistence-changes/historical-formats/v1${suffix}`
- const original = readFileSync(join(root, path), 'utf8')
- for (const position of [':36', ':36:2', '#L36', '#L36-L38']) {
- write(root, path, original + `\nSource: \`packages/core/session/src/types.ts${position}\`.\n`)
- expect(() => loadPersistenceFormats(root)).toThrow('historical source references must omit line numbers')
- }
- write(root, path, original + '\nSource: `packages/core/session/src/types.ts`.\n')
- expect(loadPersistenceFormats(root).entries).toHaveLength(4)
- })
- it('loads a contiguous archive and the current catalog without Git or historical package sources', () => {
- const formats = loadPersistenceFormats(fixture())
- expect(formats.currentVersion).toBe(3)
- expect(formats.entries.map(entry => entry.version)).toEqual([0, 1, 2, 3])
- expect(formats.entries[0]?.source).toEqual({ tag: 'dsh-v0.1.0-rc.5' })
- expect(formats.entries[1]?.source).toEqual({ pullRequest: 3349 })
- expect(formats.entries[3]).toMatchObject({ document: 'docs/persistence-catalog.md', schemaPath: 'docs/persistence-schema.json' })
- expect(formats.entries[3]?.source).toBeUndefined()
- })
- it('accepts a version-zero writer without a historical directory', () => {
- const root = fixture(0)
- rmSync(join(root, 'docs/persistence-changes/historical-formats'), { recursive: true })
- expect(loadPersistenceFormats(root).entries).toHaveLength(1)
- })
- it.each(['.md', '.zh.md', '.schema.json'])('requires every historical companion %s', (suffix) => {
- const root = fixture()
- rmSync(join(root, `docs/persistence-changes/historical-formats/v1${suffix}`))
- expect(() => loadPersistenceFormats(root)).toThrow(`missing persistence format artifact v1${suffix}`)
- })
- it('rejects a gap even when all remaining versions have complete companions', () => {
- const root = fixture()
- for (const suffix of ['.md', '.zh.md', '.schema.json']) rmSync(join(root, `docs/persistence-changes/historical-formats/v1${suffix}`))
- expect(() => loadPersistenceFormats(root)).toThrow('v1: missing persistence format artifact')
- })
- it.each(['persistence-catalog.md', 'persistence-catalog.zh.md', 'persistence-schema.json'])('requires the current reference artifact %s', (filename) => {
- const root = fixture()
- rmSync(join(root, `docs/${filename}`))
- expect(() => loadPersistenceFormats(root)).toThrow(`missing persistence format artifact docs/${filename}`)
- })
- it('requires a formerly current archive when the writer advances', () => {
- const root = fixture()
- write(root, 'packages/core/session/src/types.ts', 'export const SESSION_FORMAT_VERSION = 4 as const\n')
- saveReference(root, 4, 4)
- expect(() => loadPersistenceFormats(root)).toThrow('v3: missing persistence format artifact')
- saveReference(root, 3, 4)
- expect(loadPersistenceFormats(root).entries.map(entry => entry.version)).toEqual([0, 1, 2, 3, 4])
- })
- it.each(['v3.md', 'v9.schema.json', 'v01.md', 'v-1.md', 'v2.schema.yaml', 'v3.i18n.yaml'])('rejects unexpected or duplicate format filename %s', (filename) => {
- const root = fixture()
- write(root, `docs/persistence-changes/historical-formats/${filename}`, '{}')
- expect(() => loadPersistenceFormats(root)).toThrow(`unexpected persistence format artifact ${filename}`)
- })
- it('rejects bilingual declarations that differ', () => {
- const root = fixture()
- edit(root, 'docs/persistence-changes/historical-formats/v1.zh.md', 'pullRequest: 3349', 'pullRequest: 3397')
- expect(() => loadPersistenceFormats(root)).toThrow('bilingual machine records differ')
- })
- it.each([
- ['kind: persistence-format', 'kind: persistence-release', 'frontmatter kind'],
- ['sessionFormatVersion: 1', 'sessionFormatVersion: 0', 'must match its filename'],
- ['schemaVersion: 1', 'schemaVersion: 2', 'unsupported persistence format record schema version'],
- ['pullRequest: 3349', 'pullRequest: 0', 'positive integer'],
- ['pullRequest: 3349', 'pullRequest: 1.5', 'positive integer'],
- ['pullRequest: 3349', 'pullRequest: 3349\n tag: dsh-v0.1.0', 'expected fields'],
- ['pullRequest: 3349', 'other: 3349', 'expected fields'],
- ['source:\n pullRequest: 3349', 'source: null', 'expected an object'],
- ['schemaVersion: 1', 'schemaVersion: 1\nunknown: true', 'expected fields'],
- ])('rejects inconsistent machine data: %s', (from, to, error) => {
- const root = fixture()
- editPair(root, 1, from, to)
- expect(() => loadPersistenceFormats(root)).toThrow(error)
- })
- it('rejects a source tag outside the DSH release namespace', () => {
- const root = fixture()
- editPair(root, 0, 'tag: dsh-v0.1.0-rc.5', 'tag: latest')
- expect(() => loadPersistenceFormats(root)).toThrow('invalid source tag')
- })
- it.each(['missing', 'extra', 'wrong-digest'])('rejects a %s recorded root', (variant) => {
- const root = fixture()
- const digest = inventory(1).roots[0]!.digest
- const from = variant === 'extra' ? 'roots:\n' : ` SessionHeader: ${digest}\n`
- const to = variant === 'missing' ? '' : variant === 'extra' ? `roots:\n event:undeclared: ${'f'.repeat(64)}\n`
- : ` SessionHeader: ${'f'.repeat(64)}\n`
- editPair(root, 1, from, to)
- expect(() => loadPersistenceFormats(root)).toThrow('recorded roots do not match the complete schema inventory')
- })
- it.each(['unknown-key', 'invalid-digest', 'duplicate-key', 'not-a-mapping'])('rejects invalid recorded roots: %s', (variant) => {
- const root = fixture()
- const digest = inventory(1).roots[0]!.digest
- const from = variant === 'not-a-mapping' ? dump({ roots: Object.fromEntries(inventory(1).roots.map(root => [root.key, root.digest])) })
- : ` SessionHeader: ${digest}\n`
- const to = variant === 'unknown-key' ? ` unknown: ${digest}\n` : variant === 'invalid-digest' ? ' SessionHeader: invalid\n'
- : variant === 'duplicate-key' ? from + from : 'roots: []\n'
- editPair(root, 1, from, to)
- expect(() => loadPersistenceFormats(root)).toThrow(variant === 'duplicate-key' ? 'duplicated mapping key'
- : variant === 'not-a-mapping' ? 'roots must be a mapping' : 'invalid recorded root')
- })
- it.each(['missing', 'duplicate'])('rejects a %s machine block', (variant) => {
- const root = fixture()
- for (const suffix of ['.md', '.zh.md']) {
- const path = `docs/persistence-changes/historical-formats/v1${suffix}`
- const text = readFileSync(join(root, path), 'utf8')
- write(root, path, variant === 'missing' ? text.replace('```yaml persistence-format', '```yaml') : text + '\n```yaml persistence-format\n{}\n```\n')
- }
- expect(() => loadPersistenceFormats(root)).toThrow('expected exactly one persistence-format machine record')
- })
- it.each(['SessionHeader', 'JsonlHeaderLine', 'SessionEventEnvelope', 'event:example/value'])('rejects an inventory missing %s', (key) => {
- const root = fixture()
- replaceSnapshot(root, 1, 3, completeInventory(inventory(1).roots.filter(root => root.key !== key)))
- expect(() => loadPersistenceFormats(root)).toThrow(key.startsWith('event:') ? 'must include an event root' : `missing schema root ${key}`)
- })
- it.each([0, 1, 2, 3])('rejects a mismatched header version at v%s', (version) => {
- const root = fixture()
- replaceSnapshot(root, version, 3, completeInventory(inventory(version).roots.map(root => root.key === 'SessionHeader' ? header(root.key, version + 1) : root)))
- expect(() => loadPersistenceFormats(root)).toThrow('SessionHeader.version must match')
- })
- it('retains historical number declarations but requires the current logical header literal', () => {
- const root = fixture()
- for (const version of [0, 1, 2]) {
- saveReference(root, version, 3, completeInventory(inventory(version).roots.map(root => root.key === 'SessionHeader' ? header(root.key, 'number') : root)))
- }
- expect(loadPersistenceFormats(root).entries).toHaveLength(4)
- replaceSnapshot(root, 3, 3, completeInventory(inventory(3).roots.map(root => root.key === 'SessionHeader' ? header(root.key, 'number') : root)))
- expect(() => loadPersistenceFormats(root)).toThrow('SessionHeader.version must match the current writer version')
- })
- it.each(['SessionHeader', 'JsonlHeaderLine'])('requires a present %s version property', (key) => {
- const root = fixture()
- replaceSnapshot(root, 1, 3, completeInventory(inventory(1).roots.map(root => root.key === key ? header(key, 1, true) : root)))
- expect(() => loadPersistenceFormats(root)).toThrow(`${key}.version must match`)
- })
- it('uses historical surface rules only for historical references', () => {
- const root = fixture()
- const event = rootSchema({ key: 'event:example/value', kind: 'event', event: 'example/value', surface: true }, [
- { kind: 'object', indices: [], properties: [
- { name: 'type', type: 1, optional: false }, { name: 'surfaceOp', type: 2, optional: true },
- ] },
- { kind: 'literal', value: 'example/value' }, { kind: 'primitive', type: 'string' },
- ])
- saveReference(root, 1, 3, completeInventory(inventory(1).roots.map(root => root.kind === 'event' ? event : root)))
- expect(loadPersistenceFormats(root).entries).toHaveLength(4)
- replaceSnapshot(root, 3, 3, completeInventory(inventory(3).roots.map(root => root.kind === 'event' ? event : root)))
- expect(() => loadPersistenceFormats(root)).toThrow('surface metadata does not match its schema')
- })
- it.each(['missing', 'duplicate', 'extra'])('rejects %s reachable type coverage', (variant) => {
- const root = fixture()
- const original = inventory(1)
- const extraSchema = canonicalizeSchema([{ kind: 'literal', value: 'unreferenced' }], 0)
- const types = variant === 'missing' ? original.types.slice(1)
- : variant === 'duplicate' ? [...original.types, original.types[0]!]
- : [...original.types, { schema: extraSchema, digest: schemaDigest(extraSchema), names: [], sources: [] }]
- replaceSnapshot(root, 1, 3, { ...original, types })
- expect(() => loadPersistenceFormats(root)).toThrow(variant === 'missing' ? 'cover every reachable type' : variant === 'duplicate' ? 'duplicate schema type' : 'unreferenced schema type')
- })
- it('permits extractor-retained unreachable types only in the current inventory', () => {
- const root = fixture()
- const original = inventory(3)
- const schema = canonicalizeSchema([{ kind: 'literal', value: false }], 0)
- replaceSnapshot(root, 3, 3, {
- ...original, types: [...original.types, { schema, digest: schemaDigest(schema), names: [], sources: [] }],
- })
- expect(loadPersistenceFormats(root).entries).toHaveLength(4)
- })
- it.each(['duplicate-root', 'invalid-digest', 'invalid-graph', 'unexpected-field'])('rejects malformed schema inventory: %s', (variant) => {
- const root = fixture()
- const original = inventory(1)
- const first = original.roots[0]!
- const changed = variant === 'duplicate-root' ? { ...original, roots: [...original.roots, first] }
- : variant === 'invalid-digest' ? { ...original, roots: [{ ...first, digest: '0'.repeat(64) }, ...original.roots.slice(1)] }
- : variant === 'invalid-graph' ? { ...original, roots: [{ ...first, schema: { root: 0, nodes: [{ kind: 'array', element: 99 }] } }, ...original.roots.slice(1)] }
- : { ...original, extra: true }
- write(root, schemaPath(1, 3), JSON.stringify(changed))
- expect(() => loadPersistenceFormats(root)).toThrow(variant === 'duplicate-root' ? 'duplicate schema root' : variant === 'invalid-digest' ? 'schema digest mismatch' : variant === 'invalid-graph' ? 'unknown schema node' : 'unknown field extra')
- })
- it.each(['persistence-changes/historical-formats/v1.md', 'persistence-changes/historical-formats/v1.zh.md', 'persistence-catalog.md', 'persistence-catalog.zh.md'])('requires the matching schema link in %s', (document) => {
- const root = fixture()
- const schema = document.startsWith('persistence-changes/historical-formats/') ? 'v1.schema.json' : 'persistence-schema.json'
- edit(root, `docs/${document}`, `](${schema})`, '](wrong.schema.json)')
- expect(() => loadPersistenceFormats(root)).toThrow(`missing link to ${schema}`)
- })
- it.each(['persistence-catalog.md', 'persistence-catalog.zh.md'])('rejects a placeholder current catalog in %s', (document) => {
- const root = fixture()
- write(root, `docs/${document}`, '# Placeholder\n\n[Inventory](persistence-schema.json)\n')
- expect(() => loadPersistenceFormats(root)).toThrow('missing schema index entry for SessionHeader')
- })
- it('checks generated schema regions and refreshes them without changing the recorded schemas or authored context', () => {
- const root = fixture()
- prepareFacts(root)
- const paths = [0, 1, 2, 3].map(version => schemaPath(version, 3))
- const before = paths.map(path => readFileSync(join(root, path), 'utf8'))
- expect(() => runPersistenceFormats([], root)).toThrow('Stale persistence format facts')
- expect(runPersistenceFormats(['--write'], root)).toContain('Refreshed')
- expect(runPersistenceFormats([], root)).toBe('Persistence formats: v0 through v3 verified (4 complete references).')
- expect(paths.map(path => readFileSync(join(root, path), 'utf8'))).toEqual(before)
- const index = readFileSync(join(root, 'docs/persistence-changes/historical-formats/README.md'), 'utf8')
- expect(index).toContain('Authored context.')
- expect(index).toContain('[V1](v1.md) | [JSON](v1.schema.json)')
- expect(index).toContain('[Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json)')
- const translatedIndex = readFileSync(join(root, 'docs/persistence-changes/historical-formats/README.zh.md'), 'utf8')
- expect(translatedIndex).toContain('[V1](v1.zh.md) | [JSON](v1.schema.json)')
- expect(translatedIndex).toContain('[当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json)')
- const english = readFileSync(join(root, 'docs/persistence-changes/historical-formats/v1.md'), 'utf8')
- expect(english).toContain('pullRequest: 3349')
- const [visible, collapsed] = english.split('<details>')
- expect(visible).toContain('\n## Complete schemas\n')
- expect(visible).toContain('\n### Persistence type fingerprints\n')
- expect(collapsed).toContain('\n### Resolved persistence types\n')
- expect(collapsed).toContain('\n#### `SessionHeader`\n')
- expect(english.match(/^## /gmu)).toHaveLength(1)
- expect(english.match(/^### /gmu)).toHaveLength(2)
- expect(english.match(/^#### /gmu)).toHaveLength(inventory(1).types.length)
- edit(root, 'docs/persistence-changes/historical-formats/v1.md', '<summary>Complete resolved types</summary>', '<summary>Missing definitions</summary>')
- expect(() => runPersistenceFormats([], root)).toThrow('Stale persistence format facts')
- })
- it('renders a format index for a current version-zero writer', () => {
- const root = fixture(0)
- prepareFacts(root, 0)
- runPersistenceFormats(['--write'], root)
- expect(runPersistenceFormats([], root)).toBe('Persistence formats: v0 through v0 verified (1 complete reference).')
- })
- it.each(['missing', 'stale'])('rejects a %s format pairing record and refreshes it with --write', (variant) => {
- const root = fixture()
- prepareFacts(root)
- runPersistenceFormats(['--write'], root)
- const sidecar = 'docs/persistence-changes/historical-formats/v1.i18n.yaml'
- if (variant === 'missing') rmSync(join(root, sidecar))
- else write(root, sidecar, 'schemaVersion: 1\n')
- expect(() => runPersistenceFormats([], root)).toThrow(`Stale persistence format facts: ${sidecar}`)
- runPersistenceFormats(['--write'], root)
- expect(() => runPersistenceFormats([], root)).not.toThrow()
- })
- it('refuses to refresh facts until every format reference passes validation', () => {
- const root = fixture()
- prepareFacts(root)
- const index = readFileSync(join(root, 'docs/persistence-changes/historical-formats/README.md'), 'utf8')
- rmSync(join(root, 'docs/persistence-changes/historical-formats/v1.schema.json'))
- expect(() => runPersistenceFormats(['--write'], root)).toThrow('missing persistence format artifact')
- expect(readFileSync(join(root, 'docs/persistence-changes/historical-formats/README.md'), 'utf8')).toBe(index)
- })
- it('refuses to refresh an archive after an event and all its types are removed from its schema', () => {
- const root = fixture()
- const retained = rootSchema({ key: 'event:example/retained', kind: 'event', event: 'example/retained', surface: false }, [
- { kind: 'object', indices: [], properties: [{ name: 'type', type: 1, optional: false }] },
- { kind: 'literal', value: 'example/retained' },
- ])
- const complete = completeInventory([...inventory(1).roots, retained])
- saveReference(root, 1, 3, complete)
- prepareFacts(root)
- runPersistenceFormats(['--write'], root)
- const document = 'docs/persistence-changes/historical-formats/v1.md'
- const before = readFileSync(join(root, document), 'utf8')
- replaceSnapshot(root, 1, 3, completeInventory(complete.roots.filter(root => root.key !== 'event:example/value')))
- expect(() => runPersistenceFormats(['--write'], root)).toThrow('recorded roots do not match the complete schema inventory')
- expect(readFileSync(join(root, document), 'utf8')).toBe(before)
- })
- })
|