contract.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. /**
  2. * Shared KV-backend conformance suite. Each backend's spec file calls
  3. * {@link runKvBackendContract} with a factory bound to its own medium; the
  4. * suite asserts every clause of the `src/backend.ts` contract so both
  5. * backends are held to identical semantics.
  6. * @module
  7. */
  8. import { describe, expect, it } from 'vitest'
  9. import type { KvUnitDescriptor, StorageBackend } from '../src/backend.ts'
  10. /** One conformance run: a fresh backend plus a way to reopen the same medium (crash simulation). */
  11. export interface KvBackendContractHarness {
  12. /** The backend under test, freshly created over an empty medium. */
  13. backend: StorageBackend
  14. /** Open a NEW backend instance over the SAME medium, as after a process restart. */
  15. reopen(): Promise<StorageBackend>
  16. }
  17. const DESCRIPTOR: KvUnitDescriptor = {
  18. name: 'contract_unit',
  19. version: 3,
  20. tables: ['alpha', 'beta'],
  21. hasGlobal: true,
  22. }
  23. /**
  24. * Run the shared conformance suite against one backend implementation.
  25. * @param label - Suite label, e.g. `json` / `sqlite`.
  26. * @param create - Factory producing a fresh harness per test.
  27. */
  28. export function runKvBackendContract(label: string, create: () => Promise<KvBackendContractHarness>) {
  29. describe(`kv backend contract: ${label}`, () => {
  30. it('opens a missing unit as empty and serves loadAll immediately', async () => {
  31. const { backend } = await create()
  32. const unit = await backend.kv!.open(DESCRIPTOR)
  33. const snapshot = await unit.loadAll()
  34. expect(snapshot.tables).toEqual({ alpha: {}, beta: {} })
  35. expect(snapshot.global).toBeNull()
  36. await backend.close()
  37. })
  38. it('round-trips records and global durably across reopen', async () => {
  39. const harness = await create()
  40. const unit = await harness.backend.kv!.open(DESCRIPTOR)
  41. await unit.putRecord('alpha', 'k1', { n: 1 })
  42. await unit.putRecord('alpha', 'k2', { n: 2 })
  43. await unit.putRecord('beta', 'weird key / with:stuff', { ok: true })
  44. await unit.setGlobal({ counter: 7 })
  45. await harness.backend.close()
  46. const reopened = await harness.reopen()
  47. const unit2 = await reopened.kv!.open(DESCRIPTOR)
  48. const snapshot = await unit2.loadAll()
  49. expect(snapshot.tables['alpha']).toEqual({ k1: { n: 1 }, k2: { n: 2 } })
  50. expect(snapshot.tables['beta']).toEqual({ 'weird key / with:stuff': { ok: true } })
  51. expect(snapshot.global).toEqual({ counter: 7 })
  52. await reopened.close()
  53. })
  54. it('putRecord overwrites and deleteRecord is idempotent', async () => {
  55. const { backend } = await create()
  56. const unit = await backend.kv!.open(DESCRIPTOR)
  57. await unit.putRecord('alpha', 'k', { v: 'old' })
  58. await unit.putRecord('alpha', 'k', { v: 'new' })
  59. await unit.deleteRecord('alpha', 'k')
  60. await unit.deleteRecord('alpha', 'k')
  61. await unit.deleteRecord('alpha', 'never-existed')
  62. const snapshot = await unit.loadAll()
  63. expect(snapshot.tables['alpha']).toEqual({})
  64. await backend.close()
  65. })
  66. it('rejects a version mismatch on reopen without touching the data', async () => {
  67. const harness = await create()
  68. const unit = await harness.backend.kv!.open(DESCRIPTOR)
  69. await unit.putRecord('alpha', 'k', { v: 1 })
  70. await harness.backend.close()
  71. const reopened = await harness.reopen()
  72. await expect(reopened.kv!.open({ ...DESCRIPTOR, version: 4 })).rejects.toMatchObject({
  73. name: 'StorageError',
  74. code: 'version-mismatch',
  75. })
  76. // Original version still opens and still holds the data.
  77. const unit2 = await reopened.kv!.open(DESCRIPTOR)
  78. expect((await unit2.loadAll()).tables['alpha']).toEqual({ k: { v: 1 } })
  79. await reopened.close()
  80. })
  81. it('rejects operations after unit close, and close is idempotent', async () => {
  82. const { backend } = await create()
  83. const unit = await backend.kv!.open(DESCRIPTOR)
  84. await unit.close()
  85. await unit.close()
  86. await expect(unit.putRecord('alpha', 'k', {})).rejects.toMatchObject({ code: 'closed' })
  87. await expect(unit.loadAll()).rejects.toMatchObject({ code: 'closed' })
  88. await backend.close()
  89. await backend.close()
  90. })
  91. })
  92. }