@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.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +65 -35
- package/README.md +21 -20
- package/dist/context.d.ts +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +5 -4
- package/dist/context.js.map +1 -1
- package/dist/derive.d.ts +14 -3
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +36 -12
- package/dist/derive.js.map +1 -1
- package/dist/draft.d.ts +28 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +156 -0
- package/dist/draft.js.map +1 -0
- package/dist/event.d.ts +4 -41
- package/dist/event.d.ts.map +1 -1
- package/dist/event.js +4 -14
- package/dist/event.js.map +1 -1
- package/dist/execute.d.ts +10 -10
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +35 -28
- package/dist/execute.js.map +1 -1
- package/dist/gate.d.ts +17 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +21 -0
- package/dist/gate.js.map +1 -0
- package/dist/index.d.ts +16 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -9
- package/dist/index.js.map +1 -1
- package/dist/internal/topo-saves.d.ts +47 -0
- package/dist/internal/topo-saves.d.ts.map +1 -0
- package/dist/internal/topo-saves.js +310 -0
- package/dist/internal/topo-saves.js.map +1 -0
- package/dist/internal/topo-store-read.d.ts +67 -0
- package/dist/internal/topo-store-read.d.ts.map +1 -0
- package/dist/internal/topo-store-read.js +222 -0
- package/dist/internal/topo-store-read.js.map +1 -0
- package/dist/internal/topo-store.d.ts +12 -0
- package/dist/internal/topo-store.d.ts.map +1 -0
- package/dist/internal/topo-store.js +571 -0
- package/dist/internal/topo-store.js.map +1 -0
- package/dist/internal/trails-db.d.ts +16 -0
- package/dist/internal/trails-db.d.ts.map +1 -0
- package/dist/internal/trails-db.js +118 -0
- package/dist/internal/trails-db.js.map +1 -0
- package/dist/layer.d.ts +5 -16
- package/dist/layer.d.ts.map +1 -1
- package/dist/layer.js +3 -20
- package/dist/layer.js.map +1 -1
- package/dist/provision-config.d.ts +22 -0
- package/dist/provision-config.d.ts.map +1 -0
- package/dist/provision-config.js +210 -0
- package/dist/provision-config.js.map +1 -0
- package/dist/provision.d.ts +71 -0
- package/dist/provision.d.ts.map +1 -0
- package/dist/provision.js +56 -0
- package/dist/provision.js.map +1 -0
- package/dist/run.d.ts +27 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +34 -0
- package/dist/run.js.map +1 -0
- package/dist/service-config.d.ts +7 -7
- package/dist/service-config.d.ts.map +1 -1
- package/dist/service-config.js +75 -73
- package/dist/service-config.js.map +1 -1
- package/dist/service.d.ts +32 -36
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +21 -21
- package/dist/service.js.map +1 -1
- package/dist/signal.d.ts +33 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +17 -0
- package/dist/signal.js.map +1 -0
- package/dist/topo-store.d.ts +48 -0
- package/dist/topo-store.d.ts.map +1 -0
- package/dist/topo-store.js +175 -0
- package/dist/topo-store.js.map +1 -0
- package/dist/topo.d.ts +10 -10
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +48 -35
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +16 -16
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +5 -4
- package/dist/trail.js.map +1 -1
- package/dist/types.d.ts +10 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.js.map +1 -1
- package/dist/validate-established-topo.d.ts +76 -0
- package/dist/validate-established-topo.d.ts.map +1 -0
- package/dist/validate-established-topo.js +43 -0
- package/dist/validate-established-topo.js.map +1 -0
- package/dist/validate-topo.d.ts +2 -2
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +34 -32
- package/dist/validate-topo.js.map +1 -1
- package/package.json +4 -1
- package/src/__tests__/context.test.ts +8 -8
- package/src/__tests__/derive.test.ts +58 -1
- package/src/__tests__/execute.test.ts +124 -122
- package/src/__tests__/{layer.test.ts → gate.test.ts} +26 -26
- package/src/__tests__/{dispatch.test.ts → run.test.ts} +39 -39
- package/src/__tests__/service-config.test.ts +41 -37
- package/src/__tests__/service.test.ts +71 -71
- package/src/__tests__/{event.test.ts → signal.test.ts} +15 -15
- package/src/__tests__/topo-store-read.test.ts +251 -0
- package/src/__tests__/topo-store.test.ts +469 -0
- package/src/__tests__/topo.test.ts +54 -54
- package/src/__tests__/trail-permit.test.ts +4 -4
- package/src/__tests__/trail.test.ts +58 -58
- package/src/__tests__/trails-db.test.ts +191 -0
- package/src/__tests__/type-utils.test.ts +3 -3
- package/src/__tests__/validate-topo.test.ts +205 -38
- package/src/context.ts +5 -4
- package/src/derive.ts +47 -16
- package/src/draft.ts +334 -0
- package/src/event.ts +11 -73
- package/src/execute.ts +56 -40
- package/src/{layer.ts → gate.ts} +13 -13
- package/src/index.ts +54 -23
- package/src/internal/topo-saves.ts +429 -0
- package/src/internal/topo-store-read.ts +473 -0
- package/src/internal/topo-store.ts +1087 -0
- package/src/internal/trails-db.ts +189 -0
- package/src/{service-config.ts → provision-config.ts} +124 -105
- package/src/provision.ts +148 -0
- package/src/{dispatch.ts → run.ts} +8 -8
- package/src/signal.ts +65 -0
- package/src/topo-store.ts +301 -0
- package/src/topo.ts +74 -52
- package/src/trail.ts +23 -22
- package/src/types.ts +11 -11
- package/src/validate-established-topo.ts +63 -0
- package/src/validate-topo.ts +38 -34
- package/tsconfig.tsbuildinfo +1 -1
- 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
|
+
});
|