archived-agent-notes.spec.ts 4.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { describe, expect, it } from 'vitest'
  2. import {
  3. extendArchiveManifest,
  4. gitBlobHash,
  5. parseArchiveManifest,
  6. renderArchiveManifest,
  7. validateArchiveArtifacts,
  8. validateArchiveManifestExtension,
  9. type ArchiveManifest,
  10. } from './archived-agent-notes.ts'
  11. import { isArchivedAgentNotePath } from './repo-files.ts'
  12. function fixture(): Map<string, Buffer> {
  13. const base = '2026-07-26-example'
  14. const source = Buffer.from(`# Agent Note: Example\n\nStatus: implemented\nArchived: 2026-07-26\n\nEnglish | [中文](${base}.zh.md)\n\n## Problem\n\nExample.\n`)
  15. const zh = Buffer.from(`# Agent Note: 示例\n\nStatus: implemented\nArchived: 2026-07-26\n\n[English](${base}.md) | 中文\n\n## 问题\n\n示例。\n`)
  16. const meta = Buffer.from(`${base}.md: ${gitBlobHash(source)}\n${base}.zh.md: ${gitBlobHash(zh)}\n`)
  17. return new Map([
  18. [`process/${base}.md`, source],
  19. [`process/${base}.zh.md`, zh],
  20. [`process/${base}.i18n.yaml`, meta],
  21. ])
  22. }
  23. describe('archived Agent Notes', () => {
  24. it('recognizes archived paths with POSIX and Windows separators', () => {
  25. expect(isArchivedAgentNotePath('.agents/notes/archived/process/example.md')).toBe(true)
  26. expect(isArchivedAgentNotePath('.agents\\notes\\archived\\process\\example.md')).toBe(true)
  27. expect(isArchivedAgentNotePath('.agents/notes/implemented/process/example.md')).toBe(false)
  28. })
  29. it('accepts one complete implemented triplet with matching archive metadata', () => {
  30. expect(validateArchiveArtifacts(fixture())).toEqual([])
  31. })
  32. it('rejects incomplete triplets and invalid archive headers', () => {
  33. const artifacts = fixture()
  34. artifacts.delete('process/2026-07-26-example.i18n.yaml')
  35. artifacts.set(
  36. 'process/2026-07-26-example.md',
  37. Buffer.from('# Agent Note: Example\n\nStatus: proposed\nArchived: yesterday\n'),
  38. )
  39. expect(validateArchiveArtifacts(artifacts).join('\n')).toMatch(/incomplete archived triplet/)
  40. })
  41. it('extends the manifest without permitting a sealed change or removal', () => {
  42. const artifacts = fixture()
  43. const empty: ArchiveManifest = { version: 1, files: {} }
  44. const first = extendArchiveManifest(empty, artifacts)
  45. expect(first.errors).toEqual([])
  46. expect(first.added).toHaveLength(3)
  47. const sealed: ArchiveManifest = { version: 1, files: first.files }
  48. const changed = new Map(artifacts)
  49. changed.set('process/2026-07-26-example.md', Buffer.from('changed'))
  50. expect(extendArchiveManifest(sealed, changed).errors).toEqual([
  51. 'process/2026-07-26-example.md: sealed content hash changed',
  52. ])
  53. changed.delete('process/2026-07-26-example.zh.md')
  54. expect(extendArchiveManifest(sealed, changed).errors).toContain(
  55. 'process/2026-07-26-example.zh.md: sealed artifact is missing',
  56. )
  57. })
  58. it('rejects replacing manifest seals alongside changed archive content', () => {
  59. const artifacts = fixture()
  60. const initial = extendArchiveManifest({ version: 1, files: {} }, artifacts)
  61. const baseline: ArchiveManifest = { version: 1, files: initial.files }
  62. const path = 'process/2026-07-26-example.md'
  63. const changedArtifacts = new Map(artifacts)
  64. changedArtifacts.set(path, Buffer.from('changed'))
  65. const replacement = extendArchiveManifest({ version: 1, files: {} }, changedArtifacts)
  66. const current: ArchiveManifest = { version: 1, files: replacement.files }
  67. expect(extendArchiveManifest(current, changedArtifacts).errors).toEqual([])
  68. expect(validateArchiveManifestExtension(baseline, current)).toEqual([
  69. `${path}: sealed manifest hash changed`,
  70. ])
  71. const removed: ArchiveManifest = {
  72. version: 1,
  73. files: Object.fromEntries(Object.entries(current.files).filter(([candidate]) => candidate !== path)),
  74. }
  75. expect(validateArchiveManifestExtension(baseline, removed)).toContain(
  76. `${path}: sealed manifest entry is missing`,
  77. )
  78. })
  79. it('round-trips the deterministic manifest schema', () => {
  80. const content = renderArchiveManifest({ 'process/z.md': `sha256:${'a'.repeat(64)}` })
  81. expect(parseArchiveManifest(content)).toEqual({
  82. version: 1,
  83. files: { 'process/z.md': `sha256:${'a'.repeat(64)}` },
  84. })
  85. })
  86. })