@ontrails/core 1.0.0-beta.12 → 1.0.0-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +65 -35
  3. package/README.md +21 -20
  4. package/dist/context.d.ts +1 -1
  5. package/dist/context.d.ts.map +1 -1
  6. package/dist/context.js +5 -4
  7. package/dist/context.js.map +1 -1
  8. package/dist/derive.d.ts +14 -3
  9. package/dist/derive.d.ts.map +1 -1
  10. package/dist/derive.js +36 -12
  11. package/dist/derive.js.map +1 -1
  12. package/dist/draft.d.ts +28 -0
  13. package/dist/draft.d.ts.map +1 -0
  14. package/dist/draft.js +156 -0
  15. package/dist/draft.js.map +1 -0
  16. package/dist/event.d.ts +4 -41
  17. package/dist/event.d.ts.map +1 -1
  18. package/dist/event.js +4 -14
  19. package/dist/event.js.map +1 -1
  20. package/dist/execute.d.ts +10 -10
  21. package/dist/execute.d.ts.map +1 -1
  22. package/dist/execute.js +35 -28
  23. package/dist/execute.js.map +1 -1
  24. package/dist/gate.d.ts +17 -0
  25. package/dist/gate.d.ts.map +1 -0
  26. package/dist/gate.js +21 -0
  27. package/dist/gate.js.map +1 -0
  28. package/dist/index.d.ts +16 -9
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +14 -9
  31. package/dist/index.js.map +1 -1
  32. package/dist/internal/topo-saves.d.ts +47 -0
  33. package/dist/internal/topo-saves.d.ts.map +1 -0
  34. package/dist/internal/topo-saves.js +310 -0
  35. package/dist/internal/topo-saves.js.map +1 -0
  36. package/dist/internal/topo-store-read.d.ts +67 -0
  37. package/dist/internal/topo-store-read.d.ts.map +1 -0
  38. package/dist/internal/topo-store-read.js +222 -0
  39. package/dist/internal/topo-store-read.js.map +1 -0
  40. package/dist/internal/topo-store.d.ts +12 -0
  41. package/dist/internal/topo-store.d.ts.map +1 -0
  42. package/dist/internal/topo-store.js +571 -0
  43. package/dist/internal/topo-store.js.map +1 -0
  44. package/dist/internal/trails-db.d.ts +16 -0
  45. package/dist/internal/trails-db.d.ts.map +1 -0
  46. package/dist/internal/trails-db.js +118 -0
  47. package/dist/internal/trails-db.js.map +1 -0
  48. package/dist/layer.d.ts +5 -16
  49. package/dist/layer.d.ts.map +1 -1
  50. package/dist/layer.js +3 -20
  51. package/dist/layer.js.map +1 -1
  52. package/dist/provision-config.d.ts +22 -0
  53. package/dist/provision-config.d.ts.map +1 -0
  54. package/dist/provision-config.js +210 -0
  55. package/dist/provision-config.js.map +1 -0
  56. package/dist/provision.d.ts +71 -0
  57. package/dist/provision.d.ts.map +1 -0
  58. package/dist/provision.js +56 -0
  59. package/dist/provision.js.map +1 -0
  60. package/dist/run.d.ts +27 -0
  61. package/dist/run.d.ts.map +1 -0
  62. package/dist/run.js +34 -0
  63. package/dist/run.js.map +1 -0
  64. package/dist/service-config.d.ts +7 -7
  65. package/dist/service-config.d.ts.map +1 -1
  66. package/dist/service-config.js +75 -73
  67. package/dist/service-config.js.map +1 -1
  68. package/dist/service.d.ts +32 -36
  69. package/dist/service.d.ts.map +1 -1
  70. package/dist/service.js +21 -21
  71. package/dist/service.js.map +1 -1
  72. package/dist/signal.d.ts +33 -0
  73. package/dist/signal.d.ts.map +1 -0
  74. package/dist/signal.js +17 -0
  75. package/dist/signal.js.map +1 -0
  76. package/dist/topo-store.d.ts +48 -0
  77. package/dist/topo-store.d.ts.map +1 -0
  78. package/dist/topo-store.js +175 -0
  79. package/dist/topo-store.js.map +1 -0
  80. package/dist/topo.d.ts +10 -10
  81. package/dist/topo.d.ts.map +1 -1
  82. package/dist/topo.js +48 -35
  83. package/dist/topo.js.map +1 -1
  84. package/dist/trail.d.ts +16 -16
  85. package/dist/trail.d.ts.map +1 -1
  86. package/dist/trail.js +5 -4
  87. package/dist/trail.js.map +1 -1
  88. package/dist/types.d.ts +10 -10
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js +2 -2
  91. package/dist/types.js.map +1 -1
  92. package/dist/validate-established-topo.d.ts +76 -0
  93. package/dist/validate-established-topo.d.ts.map +1 -0
  94. package/dist/validate-established-topo.js +43 -0
  95. package/dist/validate-established-topo.js.map +1 -0
  96. package/dist/validate-topo.d.ts +2 -2
  97. package/dist/validate-topo.d.ts.map +1 -1
  98. package/dist/validate-topo.js +34 -32
  99. package/dist/validate-topo.js.map +1 -1
  100. package/package.json +4 -1
  101. package/src/__tests__/context.test.ts +8 -8
  102. package/src/__tests__/derive.test.ts +58 -1
  103. package/src/__tests__/execute.test.ts +124 -122
  104. package/src/__tests__/{layer.test.ts → gate.test.ts} +26 -26
  105. package/src/__tests__/{dispatch.test.ts → run.test.ts} +39 -39
  106. package/src/__tests__/service-config.test.ts +41 -37
  107. package/src/__tests__/service.test.ts +71 -71
  108. package/src/__tests__/{event.test.ts → signal.test.ts} +15 -15
  109. package/src/__tests__/topo-store-read.test.ts +251 -0
  110. package/src/__tests__/topo-store.test.ts +469 -0
  111. package/src/__tests__/topo.test.ts +54 -54
  112. package/src/__tests__/trail-permit.test.ts +4 -4
  113. package/src/__tests__/trail.test.ts +58 -58
  114. package/src/__tests__/trails-db.test.ts +191 -0
  115. package/src/__tests__/type-utils.test.ts +3 -3
  116. package/src/__tests__/validate-topo.test.ts +205 -38
  117. package/src/context.ts +5 -4
  118. package/src/derive.ts +47 -16
  119. package/src/draft.ts +334 -0
  120. package/src/event.ts +11 -73
  121. package/src/execute.ts +56 -40
  122. package/src/{layer.ts → gate.ts} +13 -13
  123. package/src/index.ts +54 -23
  124. package/src/internal/topo-saves.ts +429 -0
  125. package/src/internal/topo-store-read.ts +473 -0
  126. package/src/internal/topo-store.ts +1087 -0
  127. package/src/internal/trails-db.ts +189 -0
  128. package/src/{service-config.ts → provision-config.ts} +124 -105
  129. package/src/provision.ts +148 -0
  130. package/src/{dispatch.ts → run.ts} +8 -8
  131. package/src/signal.ts +65 -0
  132. package/src/topo-store.ts +301 -0
  133. package/src/topo.ts +74 -52
  134. package/src/trail.ts +23 -22
  135. package/src/types.ts +11 -11
  136. package/src/validate-established-topo.ts +63 -0
  137. package/src/validate-topo.ts +38 -34
  138. package/tsconfig.tsbuildinfo +1 -1
  139. package/src/service.ts +0 -145
@@ -0,0 +1,469 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { z } from 'zod';
7
+
8
+ import { Result, provision, signal, topo, trail } from '../index.js';
9
+ import {
10
+ ensureTopoHistorySchema,
11
+ pinTopoSave,
12
+ pruneUnpinnedTopoSaves,
13
+ } from '../internal/topo-saves.js';
14
+ import {
15
+ getStoredTopoExport,
16
+ persistEstablishedTopoSave,
17
+ } from '../internal/topo-store.js';
18
+ import { openWriteTrailsDb } from '../internal/trails-db.js';
19
+
20
+ const noop = () => Result.ok({ ok: true });
21
+
22
+ /** Unwrap a Result in tests, throwing on Err. */
23
+ const unwrap = <T>(result: Result<T, Error>): T => {
24
+ if (result.isErr()) {
25
+ throw result.error;
26
+ }
27
+ return result.value;
28
+ };
29
+
30
+ const countRows = (
31
+ db: ReturnType<typeof openWriteTrailsDb>,
32
+ tableName: string,
33
+ saveId?: string
34
+ ): number => {
35
+ const row =
36
+ saveId === undefined
37
+ ? db
38
+ .query<{ count: number }, []>(
39
+ `SELECT COUNT(*) as count FROM ${tableName}`
40
+ )
41
+ .get()
42
+ : db
43
+ .query<{ count: number }, [string]>(
44
+ `SELECT COUNT(*) as count FROM ${tableName} WHERE save_id = ?`
45
+ )
46
+ .get(saveId);
47
+ return row?.count ?? 0;
48
+ };
49
+
50
+ const tableExists = (
51
+ db: ReturnType<typeof openWriteTrailsDb>,
52
+ tableName: string
53
+ ): boolean => {
54
+ const row = db
55
+ .query<{ name: string }, [string]>(
56
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?"
57
+ )
58
+ .get(tableName);
59
+ return row?.name === tableName;
60
+ };
61
+
62
+ const exampleApp = () => {
63
+ const dbMain = provision('db.main', {
64
+ create: () => Result.ok({ source: 'factory' }),
65
+ description: 'Primary database',
66
+ health: () => Result.ok({ ok: true }),
67
+ mock: () => ({ source: 'mock' }),
68
+ });
69
+
70
+ const searchIndex = provision('search.index', {
71
+ create: () => Result.ok({ source: 'factory' }),
72
+ mock: () => ({ source: 'mock' }),
73
+ });
74
+
75
+ const entityAdded = signal('entity.added', {
76
+ description: 'An entity was added',
77
+ from: ['entity.add'],
78
+ payload: z.object({ id: z.string() }),
79
+ });
80
+
81
+ const entityAdd = trail('entity.add', {
82
+ blaze: (input: { readonly name: string }) =>
83
+ Result.ok({ id: input.name.toLowerCase(), ok: true }),
84
+ description: 'Add a new entity',
85
+ examples: [
86
+ {
87
+ expected: { id: 'ada', ok: true },
88
+ input: { name: 'Ada' },
89
+ name: 'Add Ada',
90
+ },
91
+ {
92
+ error: 'ConflictError',
93
+ input: { name: 'Existing' },
94
+ name: 'Conflict on duplicate',
95
+ },
96
+ ],
97
+ input: z.object({ name: z.string() }),
98
+ meta: { owner: 'core', tags: ['write', 'entity'] },
99
+ output: z.object({ id: z.string(), ok: z.boolean() }),
100
+ provisions: [dbMain, searchIndex],
101
+ });
102
+
103
+ const entityList = trail('entity.list', {
104
+ blaze: () => Result.ok({ items: ['ada'] }),
105
+ crosses: ['entity.add'],
106
+ description: 'List entities',
107
+ idempotent: true,
108
+ input: z.object({}),
109
+ intent: 'read',
110
+ output: z.object({ items: z.array(z.string()) }),
111
+ provisions: [dbMain],
112
+ });
113
+
114
+ return topo('projection-app', {
115
+ dbMain,
116
+ entityAdd,
117
+ entityAdded,
118
+ entityList,
119
+ searchIndex,
120
+ });
121
+ };
122
+
123
+ const readTrailRows = (
124
+ db: ReturnType<typeof openWriteTrailsDb>,
125
+ saveId: string
126
+ ) =>
127
+ db
128
+ .query<
129
+ {
130
+ description: string | null;
131
+ example_count: number;
132
+ has_output: number;
133
+ id: string;
134
+ idempotent: number;
135
+ intent: string;
136
+ meta: string | null;
137
+ },
138
+ [string]
139
+ >(
140
+ `SELECT id, intent, idempotent, has_output, example_count, description, meta
141
+ FROM topo_trails
142
+ WHERE save_id = ?
143
+ ORDER BY id ASC`
144
+ )
145
+ .all(saveId);
146
+
147
+ const readTrailSignalRows = (
148
+ db: ReturnType<typeof openWriteTrailsDb>,
149
+ saveId: string
150
+ ) =>
151
+ db
152
+ .query<{ signal_id: string; trail_id: string }, [string]>(
153
+ `SELECT trail_id, signal_id
154
+ FROM topo_trail_signals
155
+ WHERE save_id = ?`
156
+ )
157
+ .all(saveId);
158
+
159
+ const readTrailheadRows = (
160
+ db: ReturnType<typeof openWriteTrailsDb>,
161
+ saveId: string
162
+ ) =>
163
+ db
164
+ .query<{ derived_name: string; trail_id: string }, [string]>(
165
+ `SELECT trail_id, derived_name
166
+ FROM topo_trailheads
167
+ WHERE save_id = ?
168
+ ORDER BY trail_id ASC`
169
+ )
170
+ .all(saveId);
171
+
172
+ const readExampleRows = (
173
+ db: ReturnType<typeof openWriteTrailsDb>,
174
+ saveId: string
175
+ ) =>
176
+ db
177
+ .query<
178
+ {
179
+ error: string | null;
180
+ expected: string | null;
181
+ input: string;
182
+ name: string;
183
+ ordinal: number;
184
+ },
185
+ [string]
186
+ >(
187
+ `SELECT ordinal, name, input, expected, error
188
+ FROM topo_examples
189
+ WHERE save_id = ?
190
+ ORDER BY ordinal ASC`
191
+ )
192
+ .all(saveId);
193
+
194
+ const readProjectedTrailIds = (
195
+ db: ReturnType<typeof openWriteTrailsDb>,
196
+ saveId: string
197
+ ) =>
198
+ db
199
+ .query<{ id: string }, [string]>(
200
+ 'SELECT id FROM topo_trails WHERE save_id = ? ORDER BY id ASC'
201
+ )
202
+ .all(saveId)
203
+ .map((row) => row.id);
204
+
205
+ const requireStoredExport = (
206
+ db: ReturnType<typeof openWriteTrailsDb>,
207
+ saveId: string
208
+ ) => {
209
+ const stored = getStoredTopoExport(db, saveId);
210
+ if (stored === undefined) {
211
+ throw new Error(`Expected stored topo export for save "${saveId}"`);
212
+ }
213
+ return stored;
214
+ };
215
+
216
+ const expectProjectionCounts = (
217
+ db: ReturnType<typeof openWriteTrailsDb>,
218
+ saveId: string
219
+ ): void => {
220
+ expect(countRows(db, 'topo_trails', saveId)).toBe(2);
221
+ expect(countRows(db, 'topo_crossings', saveId)).toBe(1);
222
+ expect(countRows(db, 'topo_trail_provisions', saveId)).toBe(3);
223
+ expect(countRows(db, 'topo_provisions', saveId)).toBe(2);
224
+ expect(countRows(db, 'topo_signals', saveId)).toBe(1);
225
+ expect(countRows(db, 'topo_trail_signals', saveId)).toBe(1);
226
+ expect(countRows(db, 'topo_trailheads', saveId)).toBe(2);
227
+ expect(countRows(db, 'topo_examples', saveId)).toBe(2);
228
+ expect(countRows(db, 'topo_schemas', saveId)).toBe(5);
229
+ expect(countRows(db, 'topo_exports')).toBeGreaterThanOrEqual(1);
230
+ };
231
+
232
+ const expectProjectedFixtureRows = (
233
+ db: ReturnType<typeof openWriteTrailsDb>,
234
+ saveId: string
235
+ ): void => {
236
+ expect(readTrailRows(db, saveId)).toEqual([
237
+ {
238
+ description: 'Add a new entity',
239
+ example_count: 2,
240
+ has_output: 1,
241
+ id: 'entity.add',
242
+ idempotent: 0,
243
+ intent: 'write',
244
+ meta: JSON.stringify({
245
+ owner: 'core',
246
+ tags: ['write', 'entity'],
247
+ }),
248
+ },
249
+ {
250
+ description: 'List entities',
251
+ example_count: 0,
252
+ has_output: 1,
253
+ id: 'entity.list',
254
+ idempotent: 1,
255
+ intent: 'read',
256
+ meta: null,
257
+ },
258
+ ]);
259
+ expect(readTrailSignalRows(db, saveId)).toEqual([
260
+ { signal_id: 'entity.added', trail_id: 'entity.add' },
261
+ ]);
262
+ expect(readTrailheadRows(db, saveId)).toEqual([
263
+ { derived_name: 'entity add', trail_id: 'entity.add' },
264
+ { derived_name: 'entity list', trail_id: 'entity.list' },
265
+ ]);
266
+ expect(readExampleRows(db, saveId)).toEqual([
267
+ {
268
+ error: null,
269
+ expected: JSON.stringify({ id: 'ada', ok: true }),
270
+ input: JSON.stringify({ name: 'Ada' }),
271
+ name: 'Add Ada',
272
+ ordinal: 0,
273
+ },
274
+ {
275
+ error: 'ConflictError',
276
+ expected: null,
277
+ input: JSON.stringify({ name: 'Existing' }),
278
+ name: 'Conflict on duplicate',
279
+ ordinal: 1,
280
+ },
281
+ ]);
282
+ };
283
+
284
+ const simpleProjectionApp = (withList: boolean) =>
285
+ topo('projection-app', {
286
+ entityAdd: trail('entity.add', {
287
+ blaze: noop,
288
+ input: z.object({}),
289
+ output: z.object({ ok: z.boolean() }),
290
+ }),
291
+ ...(withList
292
+ ? {
293
+ entityList: trail('entity.list', {
294
+ blaze: () => Result.ok({ items: ['one'] }),
295
+ input: z.object({}),
296
+ intent: 'read',
297
+ output: z.object({ items: z.array(z.string()) }),
298
+ }),
299
+ }
300
+ : {}),
301
+ });
302
+
303
+ const seedHistoryOnlyTopoSchema = (
304
+ db: ReturnType<typeof openWriteTrailsDb>
305
+ ): void => {
306
+ db.run(
307
+ `INSERT INTO meta_schema_versions (subsystem, version, updated_at)
308
+ VALUES ('topo', 1, ?)`,
309
+ ['2026-04-03T11:00:00.000Z']
310
+ );
311
+ db.run(`CREATE TABLE IF NOT EXISTS topo_saves (
312
+ id TEXT PRIMARY KEY,
313
+ git_sha TEXT,
314
+ git_dirty INTEGER NOT NULL DEFAULT 0,
315
+ trail_count INTEGER NOT NULL DEFAULT 0,
316
+ signal_count INTEGER NOT NULL DEFAULT 0,
317
+ provision_count INTEGER NOT NULL DEFAULT 0,
318
+ created_at TEXT NOT NULL
319
+ )`);
320
+ db.run(`CREATE TABLE IF NOT EXISTS topo_pins (
321
+ name TEXT PRIMARY KEY,
322
+ save_id TEXT NOT NULL UNIQUE,
323
+ created_at TEXT NOT NULL
324
+ )`);
325
+ db.run(
326
+ `INSERT INTO topo_saves (
327
+ id, git_sha, git_dirty, trail_count, signal_count, provision_count, created_at
328
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`,
329
+ ['seed-save', 'seed123', 0, 1, 0, 0, '2026-04-03T11:00:00.000Z']
330
+ );
331
+ db.run('INSERT INTO topo_pins (name, save_id, created_at) VALUES (?, ?, ?)', [
332
+ 'seed-pin',
333
+ 'seed-save',
334
+ '2026-04-03T11:01:00.000Z',
335
+ ]);
336
+ };
337
+
338
+ describe('topo store projection', () => {
339
+ let tmpRoot: string | undefined;
340
+
341
+ afterEach(() => {
342
+ if (tmpRoot) {
343
+ rmSync(tmpRoot, { force: true, recursive: true });
344
+ tmpRoot = undefined;
345
+ }
346
+ });
347
+
348
+ const makeRoot = (): string => {
349
+ tmpRoot = mkdtempSync(join(tmpdir(), 'topo-store-'));
350
+ return tmpRoot;
351
+ };
352
+
353
+ const withProjectionDb = (
354
+ run: (db: ReturnType<typeof openWriteTrailsDb>) => void
355
+ ): void => {
356
+ const db = openWriteTrailsDb({ rootDir: makeRoot() });
357
+ try {
358
+ run(db);
359
+ } finally {
360
+ db.close();
361
+ }
362
+ };
363
+
364
+ test('projects a save-scoped relational topo from the established app graph', () => {
365
+ withProjectionDb((db) => {
366
+ const save = unwrap(
367
+ persistEstablishedTopoSave(db, exampleApp(), {
368
+ createdAt: '2026-04-03T12:00:00.000Z',
369
+ gitDirty: false,
370
+ gitSha: 'abc123',
371
+ })
372
+ );
373
+ expectProjectionCounts(db, save.id);
374
+ expectProjectedFixtureRows(db, save.id);
375
+
376
+ const stored = requireStoredExport(db, save.id);
377
+ expect(JSON.parse(stored.trailheadMapJson)).toMatchObject({
378
+ entries: expect.any(Array),
379
+ generatedAt: '2026-04-03T12:00:00.000Z',
380
+ version: '1.0',
381
+ });
382
+ expect(JSON.parse(stored.lockContent)).toMatchObject({
383
+ apps: {
384
+ 'projection-app': {
385
+ provisions: expect.any(Object),
386
+ signals: expect.any(Object),
387
+ trails: expect.any(Object),
388
+ },
389
+ },
390
+ generatedAt: '2026-04-03T12:00:00.000Z',
391
+ hash: stored.trailheadHash,
392
+ version: 1,
393
+ });
394
+ });
395
+ });
396
+
397
+ test('keeps projected rows isolated across successive saves', () => {
398
+ withProjectionDb((db) => {
399
+ const firstSave = unwrap(
400
+ persistEstablishedTopoSave(db, simpleProjectionApp(false), {
401
+ createdAt: '2026-04-03T12:00:00.000Z',
402
+ })
403
+ );
404
+ const secondSave = unwrap(
405
+ persistEstablishedTopoSave(db, simpleProjectionApp(true), {
406
+ createdAt: '2026-04-03T12:05:00.000Z',
407
+ })
408
+ );
409
+
410
+ expect(firstSave.id).not.toBe(secondSave.id);
411
+ expect(countRows(db, 'topo_trails', firstSave.id)).toBe(1);
412
+ expect(countRows(db, 'topo_trails', secondSave.id)).toBe(2);
413
+ expect(readProjectedTrailIds(db, firstSave.id)).toEqual(['entity.add']);
414
+ expect(readProjectedTrailIds(db, secondSave.id)).toEqual([
415
+ 'entity.add',
416
+ 'entity.list',
417
+ ]);
418
+ expect(requireStoredExport(db, firstSave.id).trailheadHash).not.toBe(
419
+ requireStoredExport(db, secondSave.id).trailheadHash
420
+ );
421
+ });
422
+ });
423
+
424
+ test("pruning an unpinned save removes only that save's projected rows", () => {
425
+ withProjectionDb((db) => {
426
+ const app = exampleApp();
427
+ const pinned = unwrap(
428
+ persistEstablishedTopoSave(db, app, {
429
+ createdAt: '2026-04-03T12:00:00.000Z',
430
+ })
431
+ );
432
+ pinTopoSave(db, { name: 'before-auth', saveId: pinned.id });
433
+
434
+ const disposable = unwrap(
435
+ persistEstablishedTopoSave(db, app, {
436
+ createdAt: '2026-04-03T12:05:00.000Z',
437
+ })
438
+ );
439
+
440
+ expect(pruneUnpinnedTopoSaves(db, { keep: 0 })).toBe(1);
441
+ expectProjectionCounts(db, pinned.id);
442
+ expect(countRows(db, 'topo_trails', disposable.id)).toBe(0);
443
+ expect(countRows(db, 'topo_crossings', disposable.id)).toBe(0);
444
+ expect(countRows(db, 'topo_examples', disposable.id)).toBe(0);
445
+ expect(countRows(db, 'topo_schemas', disposable.id)).toBe(0);
446
+ });
447
+ });
448
+
449
+ test('upgrades a history-only topo schema to the projected topo schema', () => {
450
+ withProjectionDb((db) => {
451
+ seedHistoryOnlyTopoSchema(db);
452
+ ensureTopoHistorySchema(db);
453
+ expect(
454
+ db
455
+ .query<{ version: number }, []>(
456
+ "SELECT version FROM meta_schema_versions WHERE subsystem = 'topo'"
457
+ )
458
+ .get()?.version
459
+ ).toBe(3);
460
+ expect(tableExists(db, 'topo_trails')).toBe(true);
461
+ expect(tableExists(db, 'topo_crossings')).toBe(true);
462
+ expect(tableExists(db, 'topo_examples')).toBe(true);
463
+ expect(tableExists(db, 'topo_exports')).toBe(true);
464
+ expect(tableExists(db, 'topo_schemas')).toBe(true);
465
+ expect(countRows(db, 'topo_saves')).toBe(1);
466
+ expect(countRows(db, 'topo_pins')).toBe(1);
467
+ });
468
+ });
469
+ });