|
1 | 1 | // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
2 | 2 |
|
3 | | -import { describe, it, expect } from 'vitest'; |
| 3 | +import { describe, it, expect, vi } from 'vitest'; |
| 4 | +import { CREATION_ATTESTED_MIGRATION_IDS } from '@objectstack/spec/system'; |
4 | 5 | import { |
5 | 6 | readDataMigrationFlag, |
6 | 7 | isDataMigrationVerified, |
7 | 8 | recordDataMigrationRun, |
| 9 | + attestFreshDatastore, |
| 10 | + CREATION_ATTESTATION_DETAIL, |
8 | 11 | type MigrationFlagEngine, |
9 | 12 | } from './migration-flag.js'; |
10 | 13 |
|
@@ -112,3 +115,85 @@ describe('deployment-level data-migration flags (#3617)', () => { |
112 | 115 | expect(await isDataMigrationVerified(engine, MIGRATION)).toBe(false); |
113 | 116 | }); |
114 | 117 | }); |
| 118 | + |
| 119 | +describe('fresh-datastore attestation (ADR-0104, 2026-07-30 addendum)', () => { |
| 120 | + it('attests every creation-attested migration, verified and blocking-free', async () => { |
| 121 | + const engine = fakeEngine(); |
| 122 | + |
| 123 | + const attested = await attestFreshDatastore(engine); |
| 124 | + |
| 125 | + expect(attested).toEqual([...CREATION_ATTESTED_MIGRATION_IDS]); |
| 126 | + expect(engine.tables.sys_migration).toHaveLength(CREATION_ATTESTED_MIGRATION_IDS.length); |
| 127 | + for (const id of CREATION_ATTESTED_MIGRATION_IDS) { |
| 128 | + expect(await isDataMigrationVerified(engine, id)).toBe(true); |
| 129 | + } |
| 130 | + }); |
| 131 | + |
| 132 | + it('records nothing as applied — no backfill ran, and none was needed', async () => { |
| 133 | + const engine = fakeEngine(); |
| 134 | + |
| 135 | + await attestFreshDatastore(engine); |
| 136 | + |
| 137 | + const row = engine.tables.sys_migration[0]; |
| 138 | + expect(row.applied_at).toBeNull(); |
| 139 | + expect(row.blocking).toBe(0); |
| 140 | + }); |
| 141 | + |
| 142 | + it('marks the row so evidence-by-birth is distinguishable from evidence-by-scan', async () => { |
| 143 | + const engine = fakeEngine(); |
| 144 | + |
| 145 | + await attestFreshDatastore(engine); |
| 146 | + |
| 147 | + expect(JSON.parse(String(engine.tables.sys_migration[0].details))).toEqual( |
| 148 | + CREATION_ATTESTATION_DETAIL, |
| 149 | + ); |
| 150 | + }); |
| 151 | + |
| 152 | + /** |
| 153 | + * The load-bearing safety property. An existing row means this store is not |
| 154 | + * one being created — whatever the caller believed — so attestation must |
| 155 | + * leave it exactly as it found it. Overwriting could only ever RAISE a gate |
| 156 | + * the deployment's own evidence had closed. |
| 157 | + */ |
| 158 | + it('never overwrites an existing row, including one a failed run closed', async () => { |
| 159 | + const engine = fakeEngine([ |
| 160 | + { id: MIGRATION, last_run_at: 'yesterday', verified_at: null, blocking: 7 }, |
| 161 | + ]); |
| 162 | + |
| 163 | + const attested = await attestFreshDatastore(engine); |
| 164 | + |
| 165 | + expect(attested).not.toContain(MIGRATION); |
| 166 | + const row = engine.tables.sys_migration.find((r) => r.id === MIGRATION)!; |
| 167 | + expect(row).toMatchObject({ verified_at: null, blocking: 7, last_run_at: 'yesterday' }); |
| 168 | + expect(await isDataMigrationVerified(engine, MIGRATION)).toBe(false); |
| 169 | + }); |
| 170 | + |
| 171 | + it('is idempotent — a second call adds nothing', async () => { |
| 172 | + const engine = fakeEngine(); |
| 173 | + |
| 174 | + await attestFreshDatastore(engine); |
| 175 | + const second = await attestFreshDatastore(engine); |
| 176 | + |
| 177 | + expect(second).toEqual([]); |
| 178 | + expect(engine.tables.sys_migration).toHaveLength(CREATION_ATTESTED_MIGRATION_IDS.length); |
| 179 | + }); |
| 180 | + |
| 181 | + it('does nothing when sys_migration is not registered (bare kernel)', async () => { |
| 182 | + const engine = fakeEngine([], { registered: false }); |
| 183 | + |
| 184 | + expect(await attestFreshDatastore(engine)).toEqual([]); |
| 185 | + expect(engine.tables.sys_migration).toHaveLength(0); |
| 186 | + }); |
| 187 | + |
| 188 | + it('a write failure warns and leaves the deployment lax — it never throws into a boot', async () => { |
| 189 | + const engine = fakeEngine(); |
| 190 | + engine.insert = async () => { |
| 191 | + throw new Error('table locked'); |
| 192 | + }; |
| 193 | + const logger = { info: vi.fn(), warn: vi.fn() }; |
| 194 | + |
| 195 | + await expect(attestFreshDatastore(engine, { logger })).resolves.toEqual([]); |
| 196 | + expect(logger.warn).toHaveBeenCalled(); |
| 197 | + expect(logger.info).not.toHaveBeenCalled(); |
| 198 | + }); |
| 199 | +}); |
0 commit comments