-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathroles.test.ts
More file actions
527 lines (444 loc) · 21.5 KB
/
Copy pathroles.test.ts
File metadata and controls
527 lines (444 loc) · 21.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
/**
* Unit tests for classifyNodeRoles in src/structure.js
*
* Uses an in-memory SQLite database with hand-crafted nodes/edges
* to verify each role classification.
*
* Test graph:
* entryFn - exported (cross-file caller), fan_in=0 from non-test → entry
* coreFn - high fan_in, low fan_out → core
* utilityFn - high fan_in, high fan_out → utility
* adapterFn - low fan_in, high fan_out → adapter
* deadFn - fan_in=0, not exported → dead-unresolved
* leafFn - low fan_in, low fan_out → leaf
*/
import Database from 'better-sqlite3';
import { beforeEach, describe, expect, it } from 'vitest';
import { initSchema } from '../../src/db/index.js';
import { classifyNodeRoles } from '../../src/features/structure.js';
let db: any;
function setup() {
db = new Database(':memory:');
db.pragma('journal_mode = WAL');
initSchema(db);
return db;
}
function insertNode(name, kind, file, line) {
return db
.prepare('INSERT INTO nodes (name, kind, file, line) VALUES (?, ?, ?, ?)')
.run(name, kind, file, line).lastInsertRowid;
}
function insertEdge(sourceId, targetId, kind) {
db.prepare(
'INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, 1.0, 0)',
).run(sourceId, targetId, kind);
}
/**
* Build a graph where median fan_in = 2 and median fan_out = 2.
* This allows clear high/low classification.
*/
function buildTestGraph() {
// File nodes (these should NOT get roles)
const fA = insertNode('a.js', 'file', 'a.js', 0);
const fB = insertNode('b.js', 'file', 'b.js', 0);
// Function nodes
const entryFn = insertNode('entryFn', 'function', 'a.js', 1);
const coreFn = insertNode('coreFn', 'function', 'a.js', 10);
const utilityFn = insertNode('utilityFn', 'function', 'a.js', 20);
const adapterFn = insertNode('adapterFn', 'function', 'b.js', 1);
const deadFn = insertNode('deadFn', 'function', 'b.js', 10);
const leafFn = insertNode('leafFn', 'function', 'b.js', 20);
// Helper targets for fan_out edges
const helperA = insertNode('helperA', 'function', 'a.js', 30);
const helperB = insertNode('helperB', 'function', 'a.js', 40);
const helperC = insertNode('helperC', 'function', 'b.js', 30);
const helperD = insertNode('helperD', 'function', 'b.js', 40);
// entryFn: fan_in=0, but exported (cross-file caller) → entry
// No callers from same file, but one cross-file caller
const crossCaller = insertNode('crossCaller', 'function', 'b.js', 50);
insertEdge(crossCaller, entryFn, 'calls');
// coreFn: high fan_in (3 callers), low fan_out (0) → core
insertEdge(entryFn, coreFn, 'calls');
insertEdge(adapterFn, coreFn, 'calls');
insertEdge(leafFn, coreFn, 'calls');
// utilityFn: high fan_in (3 callers), high fan_out (3 callees) → utility
insertEdge(entryFn, utilityFn, 'calls');
insertEdge(adapterFn, utilityFn, 'calls');
insertEdge(crossCaller, utilityFn, 'calls');
insertEdge(utilityFn, helperA, 'calls');
insertEdge(utilityFn, helperB, 'calls');
insertEdge(utilityFn, helperC, 'calls');
// adapterFn: low fan_in (1 caller), high fan_out (3 callees) → adapter
insertEdge(entryFn, adapterFn, 'calls');
// adapterFn already calls coreFn and utilityFn above
insertEdge(adapterFn, helperD, 'calls');
// deadFn: fan_in=0, not exported → dead
// No callers at all
// leafFn: low fan_in (1 caller), low fan_out (1 callee) → leaf
insertEdge(crossCaller, leafFn, 'calls');
// leafFn already calls coreFn above
return { fA, fB, entryFn, coreFn, utilityFn, adapterFn, deadFn, leafFn };
}
describe('classifyNodeRoles', () => {
beforeEach(() => {
setup();
});
it('classifies each role correctly', () => {
buildTestGraph();
const summary = classifyNodeRoles(db);
// Verify summary has all roles
expect(summary).toHaveProperty('entry');
expect(summary).toHaveProperty('core');
expect(summary).toHaveProperty('utility');
expect(summary).toHaveProperty('adapter');
expect(summary).toHaveProperty('dead');
expect(summary).toHaveProperty('leaf');
// Verify specific node roles
const getRole = (name) => db.prepare('SELECT role FROM nodes WHERE name = ?').get(name)?.role;
expect(getRole('deadFn')).toBe('dead-unresolved');
expect(getRole('coreFn')).toBe('core');
expect(getRole('utilityFn')).toBe('utility');
});
it('marks file and directory nodes as NULL role', () => {
buildTestGraph();
// Insert a directory node
insertNode('src', 'directory', 'src', 0);
classifyNodeRoles(db);
const fileRole = db.prepare("SELECT role FROM nodes WHERE kind = 'file' LIMIT 1").get();
expect(fileRole.role).toBeNull();
const dirRole = db.prepare("SELECT role FROM nodes WHERE kind = 'directory' LIMIT 1").get();
expect(dirRole.role).toBeNull();
});
it('is idempotent (running twice gives same results)', () => {
buildTestGraph();
const summary1 = classifyNodeRoles(db);
const roles1 = db
.prepare('SELECT name, role FROM nodes WHERE role IS NOT NULL ORDER BY name')
.all();
const summary2 = classifyNodeRoles(db);
const roles2 = db
.prepare('SELECT name, role FROM nodes WHERE role IS NOT NULL ORDER BY name')
.all();
expect(summary1).toEqual(summary2);
expect(roles1).toEqual(roles2);
});
it('handles empty graph without crashing', () => {
const summary = classifyNodeRoles(db);
expect(summary).toEqual({
entry: 0,
core: 0,
utility: 0,
adapter: 0,
dead: 0,
'dead-leaf': 0,
'dead-entry': 0,
'dead-ffi': 0,
'dead-unresolved': 0,
'test-only': 0,
leaf: 0,
});
});
it('adapts median thresholds to data', () => {
// Create a small graph: 2 functions with fan_in=[1,1], fan_out=[1,1]
// median of non-zero = 1 for both, so fan_in >= 1 = high, fan_out >= 1 = high
insertNode('a.js', 'file', 'a.js', 0);
const fn1 = insertNode('fn1', 'function', 'a.js', 1);
const fn2 = insertNode('fn2', 'function', 'a.js', 10);
// fn1 calls fn2, fn2 calls fn1 (mutual)
insertEdge(fn1, fn2, 'calls');
insertEdge(fn2, fn1, 'calls');
const summary = classifyNodeRoles(db);
// Both have fan_in=1 (>= median 1) and fan_out=1 (>= median 1) → utility
expect(summary.utility).toBe(2);
});
it('classifies nodes with only non-call edges as dead-unresolved', () => {
const fA = insertNode('a.js', 'file', 'a.js', 0);
const fn1 = insertNode('fn1', 'function', 'a.js', 1);
// Only import edge, no call edge
insertEdge(fA, fn1, 'imports');
classifyNodeRoles(db);
const role = db.prepare("SELECT role FROM nodes WHERE name = 'fn1'").get();
expect(role.role).toBe('dead-unresolved');
});
it('does not classify type-imported interfaces as dead (#840)', () => {
// Simulate: file b.ts has `import type { MyInterface } from './a'`
// This should create a symbol-level imports-type edge from b.ts file node
// to the MyInterface symbol, giving it fan-in > 0.
const fA = insertNode('a.ts', 'file', 'a.ts', 0);
const fB = insertNode('b.ts', 'file', 'b.ts', 0);
const iface = insertNode('MyInterface', 'interface', 'a.ts', 5);
// File-level imports-type edge (file → file)
insertEdge(fB, fA, 'imports-type');
// Symbol-level imports-type edge (file → symbol) — the fix creates these
insertEdge(fB, iface, 'imports-type');
classifyNodeRoles(db);
const role = db.prepare("SELECT role FROM nodes WHERE name = 'MyInterface'").get();
// Should NOT be dead — it has a type-import consumer
expect(role.role).not.toMatch(/^dead/);
});
it('classifies interface with no type-import edges as dead', () => {
insertNode('a.ts', 'file', 'a.ts', 0);
insertNode('UnusedInterface', 'interface', 'a.ts', 5);
classifyNodeRoles(db);
const role = db.prepare("SELECT role FROM nodes WHERE name = 'UnusedInterface'").get();
expect(role.role).toBe('dead-unresolved');
});
it('does not classify exported interface as dead when used only as same-file type annotation (#1583)', () => {
// Simulate: exported interface whose only usage is as a parameter type in the same file.
// No cross-file imports-type edge exists because same-file type annotations don't produce edges.
// The extractor marks the interface as exported=1. The classifier must honour that flag.
db.prepare('INSERT INTO nodes (name, kind, file, line, exported) VALUES (?, ?, ?, ?, ?)').run(
'MyOpts',
'interface',
'src/helpers.ts',
10,
1,
);
classifyNodeRoles(db);
const role = db.prepare("SELECT role FROM nodes WHERE name = 'MyOpts'").get();
// Should be entry (exported, fan-in 0), not dead-unresolved
expect(role.role).toBe('entry');
});
it('classifies non-exported interface with no callers as dead-unresolved (#1583 boundary)', () => {
// An interface without export keyword and without cross-file references is genuinely dead.
db.prepare('INSERT INTO nodes (name, kind, file, line, exported) VALUES (?, ?, ?, ?, ?)').run(
'InternalOpts',
'interface',
'src/helpers.ts',
20,
0,
);
classifyNodeRoles(db);
const role = db.prepare("SELECT role FROM nodes WHERE name = 'InternalOpts'").get();
expect(role.role).toBe('dead-unresolved');
});
it('does not classify struct/enum/trait as dead when file has active callables (#1584)', () => {
// Simulate a Rust file with struct definitions used as type parameters.
// The structs have fan_in=0 (no call edges — type annotations don't produce edges),
// but the file has active functions. The structs are almost certainly live.
insertNode('build_edges.rs', 'file', 'build_edges.rs', 0);
// An external caller that makes build_graph "active" (fan_in > 0)
const externalCaller = insertNode('main', 'function', 'main.rs', 1);
const fn1 = insertNode('build_graph', 'function', 'build_edges.rs', 10);
const fn2 = insertNode('resolve_imports', 'function', 'build_edges.rs', 50);
insertNode('NodeInfo', 'struct', 'build_edges.rs', 5);
insertNode('CallInfo', 'struct', 'build_edges.rs', 15);
insertNode('EdgeKind', 'enum', 'build_edges.rs', 25);
insertNode('Resolvable', 'trait', 'build_edges.rs', 35);
// The file has active callables: fn1 is called externally, fn1 calls fn2
insertEdge(externalCaller, fn1, 'calls');
insertEdge(fn1, fn2, 'calls');
// Structs have no call edges (they are used as type annotations only)
classifyNodeRoles(db);
const getRole = (name) => db.prepare('SELECT role FROM nodes WHERE name = ?').get(name)?.role;
// Functions are classified normally (they have edges)
expect(getRole('build_graph')).not.toMatch(/^dead/);
// Struct/enum/trait with active file siblings should be leaf, not dead
expect(getRole('NodeInfo')).toBe('leaf');
expect(getRole('CallInfo')).toBe('leaf');
expect(getRole('EdgeKind')).toBe('leaf');
expect(getRole('Resolvable')).toBe('leaf');
});
it('classifies struct with no active file siblings as dead (#1584 boundary)', () => {
// A struct in a file with no other active callables is genuinely dead.
insertNode('orphan.rs', 'file', 'orphan.rs', 0);
insertNode('OrphanStruct', 'struct', 'orphan.rs', 5);
classifyNodeRoles(db);
const role = db.prepare("SELECT role FROM nodes WHERE name = 'OrphanStruct'").get();
// No active callables in the file — the struct is dead (dead-ffi for .rs files)
expect(role.role).toMatch(/^dead/);
});
it('classifies Commander.js execute/validate methods in cli/commands/ as entry (#1585)', () => {
// Simulate the Commander.js command object pattern:
// export const command = { execute(args, opts, ctx) { ... }, validate(args) { ... } }
// These methods have fan_in=0 because Commander dispatches them dynamically.
// They must be classified as `entry`, not `dead-entry`, so they don't appear
// in `--role dead` output and don't pollute dead-code analysis.
insertNode('src/cli/commands/roles.ts', 'file', 'src/cli/commands/roles.ts', 0);
insertNode('execute', 'method', 'src/cli/commands/roles.ts', 26);
insertNode('validate', 'method', 'src/cli/commands/roles.ts', 21);
classifyNodeRoles(db);
const getRole = (name) => db.prepare('SELECT role FROM nodes WHERE name = ?').get(name)?.role;
expect(getRole('execute')).toBe('entry');
expect(getRole('validate')).toBe('entry');
});
it('does not classify execute/validate as entry when not in a framework directory (#1585 boundary)', () => {
// An `execute` method in a non-CLI file (e.g. a utility class) should NOT
// be promoted to `entry` just because of its name.
insertNode('src/utils/executor.ts', 'file', 'src/utils/executor.ts', 0);
insertNode('execute', 'method', 'src/utils/executor.ts', 10);
classifyNodeRoles(db);
const role = db.prepare("SELECT role FROM nodes WHERE name = 'execute'").get()?.role;
// Not in a framework directory — should be dead-unresolved (no callers)
expect(role).not.toBe('entry');
expect(role).toMatch(/^dead/);
});
it('does not promote sole function with fanIn=0, fanOut>0 to leaf via self-sibling (#1586 boundary)', () => {
// A function with fanIn=0, fanOut>0 that is the ONLY callable in its file
// must NOT see its own file as "active" and thereby promote itself to leaf.
// Previously buildActiveFilesSet used (fan_in > 0 || fan_out > 0), causing
// this node to add its own file to activeFiles, discover hasActiveFileSiblings=true,
// and be promoted to leaf despite having zero callers.
insertNode('src/helpers/isolated.ts', 'file', 'src/helpers/isolated.ts', 0);
const helper = insertNode('helperB', 'function', 'src/helpers/isolated.ts', 5);
// A callee for helperB so fanOut > 0
const callee = insertNode('callee', 'function', 'src/helpers/other.ts', 10);
insertEdge(helper, callee, 'calls');
classifyNodeRoles(db);
const role = db.prepare("SELECT role FROM nodes WHERE name = 'helperB'").get()?.role;
// helperB has fanIn=0, fanOut=1, sole callable in its file — must stay dead-unresolved
expect(role).toBe('dead-unresolved');
});
it('does not promote sole method with fanIn=0, fanOut>0 to leaf via self-sibling (#1586 boundary)', () => {
// Same self-sibling false-negative as above, but for a method kind.
insertNode('src/helpers/isolated2.ts', 'file', 'src/helpers/isolated2.ts', 0);
const method = insertNode('doWork', 'method', 'src/helpers/isolated2.ts', 5);
const callee = insertNode('calleeM', 'function', 'src/helpers/other2.ts', 10);
insertEdge(method, callee, 'calls');
classifyNodeRoles(db);
const role = db.prepare("SELECT role FROM nodes WHERE name = 'doWork'").get()?.role;
// doWork has fanIn=0, fanOut=1, sole callable in its file — must stay dead-unresolved
expect(role).toBe('dead-unresolved');
});
it('incremental path: does not classify exported interface as dead when used only as same-file type annotation (#1583)', () => {
// Exercises classifyNodeRolesIncremental (triggered by passing changedFiles).
// An exported=1 interface with no cross-file edges must be promoted to entry,
// not dead-unresolved, on the incremental path just as on the full path.
db.prepare('INSERT INTO nodes (name, kind, file, line, exported) VALUES (?, ?, ?, ?, ?)').run(
'IncrementalOpts',
'interface',
'src/helpers.ts',
30,
1,
);
// Pass the file as the changed-files list to trigger the incremental path.
classifyNodeRoles(db, ['src/helpers.ts']);
const role = db.prepare("SELECT role FROM nodes WHERE name = 'IncrementalOpts'").get();
// Should be entry (exported, fan-in 0), not dead-unresolved
expect(role.role).toBe('entry');
});
// ── Parameters and interface members are not dead-code targets (#1723) ──
it('excludes parameter-kind nodes from role classification entirely', () => {
// A parameter's liveness is a local dataflow question (is it referenced
// within its own function body), not a call-graph reachability question.
// "No incoming call edges" is guaranteed for every parameter regardless of
// usage, so parameters must never receive a `dead-*` role — or any role at
// all, the same treatment as file/directory nodes.
insertNode('src/helpers.ts', 'file', 'src/helpers.ts', 0);
const fn = insertNode('findChild', 'function', 'src/helpers.ts', 26);
insertNode('node', 'parameter', 'src/helpers.ts', 26);
insertNode('type', 'parameter', 'src/helpers.ts', 26);
// An external caller so findChild itself is not incidentally dead too —
// isolates the assertion to parameter handling specifically.
const caller = insertNode('caller', 'function', 'other.ts', 1);
insertEdge(caller, fn, 'calls');
classifyNodeRoles(db);
const paramRoles = db.prepare("SELECT role FROM nodes WHERE kind = 'parameter'").all() as {
role: string | null;
}[];
expect(paramRoles).toHaveLength(2);
for (const row of paramRoles) {
expect(row.role).toBeNull();
}
});
it('does not classify interface members as dead', () => {
// TS `interface ExtractParametersOptions { typeMap?: Map<...> }` extracts
// `typeMap` as a top-level `method`-kind definition named
// `ExtractParametersOptions.typeMap` (property-signature members are
// currently mislabeled `method` by the extractor — tracked separately).
// It has no callers by construction; it must not be flagged dead.
insertNode('src/helpers.ts', 'file', 'src/helpers.ts', 0);
insertNode('ExtractParametersOptions', 'interface', 'src/helpers.ts', 359);
insertNode('ExtractParametersOptions.typeMap', 'method', 'src/helpers.ts', 361);
classifyNodeRoles(db);
const role = db
.prepare("SELECT role FROM nodes WHERE name = 'ExtractParametersOptions.typeMap'")
.get() as { role: string | null };
expect(role.role).toBe('leaf');
});
it('regression: used parameters, interface members, and a genuinely dead function are classified correctly together', () => {
// Mirrors the exact issue #1723 repro: `findChild(node, type)` in
// src/extractors/helpers.ts, alongside an interface and a truly dead function.
insertNode('src/helpers.ts', 'file', 'src/helpers.ts', 0);
const findChildFn = insertNode('findChild', 'function', 'src/helpers.ts', 26);
insertNode('node', 'parameter', 'src/helpers.ts', 26);
insertNode('type', 'parameter', 'src/helpers.ts', 26);
insertNode('ChaContext', 'interface', 'src/helpers.ts', 18);
insertNode('ChaContext.implementors', 'method', 'src/helpers.ts', 20);
insertNode('trulyDeadHelper', 'function', 'src/helpers.ts', 400);
const caller = insertNode('caller', 'function', 'other.ts', 1);
insertEdge(caller, findChildFn, 'calls');
classifyNodeRoles(db);
const getRole = (name) => db.prepare('SELECT role FROM nodes WHERE name = ?').get(name)?.role;
expect(getRole('node')).toBeNull();
expect(getRole('type')).toBeNull();
expect(getRole('ChaContext.implementors')).toBe('leaf');
expect(getRole('trulyDeadHelper')).toBe('dead-unresolved');
});
// ── Chunked role-statement cache (issue #1767) ──
it('correctly writes roles for a batch spanning multiple statement chunk sizes', () => {
// batchUpdateRoles chunks writes in groups of 500. A single role with
// 650 ids exercises both a full-size (500) and a remainder-size (150)
// prepared statement from the shared chunk-keyed statement cache.
insertNode('src/big.ts', 'file', 'src/big.ts', 0);
const total = 650;
for (let i = 0; i < total; i++) {
insertNode(`field${i}`, 'property', 'src/big.ts', i);
}
const summary = classifyNodeRoles(db);
expect(summary['dead-leaf']).toBe(total);
expect(summary.dead).toBe(total);
const count = db
.prepare("SELECT COUNT(*) AS cnt FROM nodes WHERE role = 'dead-leaf'")
.get() as { cnt: number };
expect(count.cnt).toBe(total);
});
it('keeps the shared chunk-keyed statement cache isolated across separate database instances', () => {
// The chunk-size statement cache (shared with domain/graph/builder/helpers.ts
// via cachedChunkStmt) is a module-level WeakMap keyed by db instance. Running
// classification against two independent in-memory databases back-to-back must
// not leak a prepared statement compiled against one db's connection into the other.
const dbA = new Database(':memory:');
dbA.pragma('journal_mode = WAL');
initSchema(dbA);
dbA
.prepare('INSERT INTO nodes (name, kind, file, line) VALUES (?, ?, ?, ?)')
.run('a.ts', 'file', 'a.ts', 0);
for (let i = 0; i < 5; i++) {
dbA
.prepare('INSERT INTO nodes (name, kind, file, line) VALUES (?, ?, ?, ?)')
.run(`fieldA${i}`, 'property', 'a.ts', i);
}
const dbB = new Database(':memory:');
dbB.pragma('journal_mode = WAL');
initSchema(dbB);
dbB
.prepare('INSERT INTO nodes (name, kind, file, line) VALUES (?, ?, ?, ?)')
.run('b.ts', 'file', 'b.ts', 0);
for (let i = 0; i < 9; i++) {
dbB
.prepare('INSERT INTO nodes (name, kind, file, line) VALUES (?, ?, ?, ?)')
.run(`fieldB${i}`, 'property', 'b.ts', i);
}
const summaryA = classifyNodeRoles(dbA);
const summaryB = classifyNodeRoles(dbB);
expect(summaryA['dead-leaf']).toBe(5);
expect(summaryB['dead-leaf']).toBe(9);
expect(
(
dbA.prepare("SELECT COUNT(*) AS cnt FROM nodes WHERE role = 'dead-leaf'").get() as {
cnt: number;
}
).cnt,
).toBe(5);
expect(
(
dbB.prepare("SELECT COUNT(*) AS cnt FROM nodes WHERE role = 'dead-leaf'").get() as {
cnt: number;
}
).cnt,
).toBe(9);
dbA.close();
dbB.close();
});
});