@tikoci/rosetta 0.6.7 → 0.6.9
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/README.md +2 -0
- package/package.json +1 -1
- package/src/browse.ts +211 -21
- package/src/db.ts +113 -0
- package/src/extract-all-versions.ts +83 -21
- package/src/extract-schema.ts +596 -0
- package/src/extract-skills.ts +390 -0
- package/src/mcp.ts +107 -8
- package/src/paths.ts +9 -2
- package/src/query.test.ts +128 -1
- package/src/query.ts +157 -9
- package/src/release.test.ts +29 -0
- package/src/schema-roundtrip.test.ts +373 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schema-roundtrip.test.ts — Tests for extract-schema.ts importer.
|
|
3
|
+
*
|
|
4
|
+
* Tests cover:
|
|
5
|
+
* - Fixture import into in-memory DB
|
|
6
|
+
* - Arch-specific node detection (x86-only, arm64-only, shared)
|
|
7
|
+
* - dir_role derivation (list, namespace, hybrid)
|
|
8
|
+
* - desc_raw parsing (string, enum, time, range, script)
|
|
9
|
+
* - _completion data landing in _attrs
|
|
10
|
+
* - schema_node_presence row counts
|
|
11
|
+
* - Legacy commands table regeneration
|
|
12
|
+
* - parseDesc() unit tests
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Database } from "bun:sqlite";
|
|
16
|
+
import { beforeAll, describe, expect, test } from "bun:test";
|
|
17
|
+
import type { FlatNode } from "./extract-schema.ts";
|
|
18
|
+
import { importSchemaNodes, mergeArchNodes, parseDesc, walk } from "./extract-schema.ts";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// In-memory DB with schema_nodes tables — mirrors initDb() schema
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
function createTestDb(): Database {
|
|
25
|
+
const db = new Database(":memory:");
|
|
26
|
+
db.run("PRAGMA journal_mode=WAL;");
|
|
27
|
+
db.run("PRAGMA foreign_keys=ON;");
|
|
28
|
+
|
|
29
|
+
// Minimal pages table for FK
|
|
30
|
+
db.run(`CREATE TABLE pages (id INTEGER PRIMARY KEY, title TEXT);`);
|
|
31
|
+
|
|
32
|
+
// schema_nodes
|
|
33
|
+
db.run(`CREATE TABLE schema_nodes (
|
|
34
|
+
id INTEGER PRIMARY KEY,
|
|
35
|
+
path TEXT NOT NULL,
|
|
36
|
+
name TEXT NOT NULL,
|
|
37
|
+
type TEXT NOT NULL,
|
|
38
|
+
parent_id INTEGER REFERENCES schema_nodes(id),
|
|
39
|
+
parent_path TEXT,
|
|
40
|
+
dir_role TEXT,
|
|
41
|
+
desc_raw TEXT,
|
|
42
|
+
data_type TEXT,
|
|
43
|
+
enum_values TEXT,
|
|
44
|
+
enum_multi INTEGER,
|
|
45
|
+
type_tag TEXT,
|
|
46
|
+
range_min TEXT,
|
|
47
|
+
range_max TEXT,
|
|
48
|
+
max_length INTEGER,
|
|
49
|
+
_arch TEXT,
|
|
50
|
+
_package TEXT,
|
|
51
|
+
_attrs TEXT,
|
|
52
|
+
page_id INTEGER REFERENCES pages(id),
|
|
53
|
+
UNIQUE(path, type)
|
|
54
|
+
);`);
|
|
55
|
+
|
|
56
|
+
db.run(`CREATE INDEX idx_sn_parent ON schema_nodes(parent_path);`);
|
|
57
|
+
db.run(`CREATE INDEX idx_sn_type ON schema_nodes(type);`);
|
|
58
|
+
db.run(`CREATE INDEX idx_sn_path ON schema_nodes(path);`);
|
|
59
|
+
|
|
60
|
+
db.run(`CREATE TABLE schema_node_presence (
|
|
61
|
+
node_id INTEGER NOT NULL REFERENCES schema_nodes(id),
|
|
62
|
+
version TEXT NOT NULL,
|
|
63
|
+
PRIMARY KEY (node_id, version)
|
|
64
|
+
);`);
|
|
65
|
+
|
|
66
|
+
db.run(`CREATE INDEX idx_snp_version ON schema_node_presence(version);`);
|
|
67
|
+
|
|
68
|
+
// Legacy compat tables
|
|
69
|
+
db.run(`CREATE TABLE commands (
|
|
70
|
+
id INTEGER PRIMARY KEY, path TEXT NOT NULL UNIQUE,
|
|
71
|
+
name TEXT NOT NULL, type TEXT NOT NULL,
|
|
72
|
+
parent_path TEXT, page_id INTEGER,
|
|
73
|
+
description TEXT, ros_version TEXT
|
|
74
|
+
);`);
|
|
75
|
+
|
|
76
|
+
db.run(`CREATE TABLE command_versions (
|
|
77
|
+
command_path TEXT NOT NULL, ros_version TEXT NOT NULL,
|
|
78
|
+
PRIMARY KEY (command_path, ros_version)
|
|
79
|
+
);`);
|
|
80
|
+
|
|
81
|
+
db.run(`CREATE TABLE ros_versions (
|
|
82
|
+
version TEXT NOT NULL, arch TEXT NOT NULL DEFAULT 'x86',
|
|
83
|
+
channel TEXT, extra_packages INTEGER NOT NULL DEFAULT 0,
|
|
84
|
+
extracted_at TEXT NOT NULL, generated_at TEXT,
|
|
85
|
+
crash_paths_tested TEXT, crash_paths_crashed TEXT,
|
|
86
|
+
crash_paths_safe TEXT, completion_stats TEXT,
|
|
87
|
+
source_url TEXT, api_transport TEXT,
|
|
88
|
+
enrichment_duration_ms INTEGER, _attrs TEXT,
|
|
89
|
+
PRIMARY KEY (version, arch)
|
|
90
|
+
);`);
|
|
91
|
+
|
|
92
|
+
return db;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Load fixtures
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
let x86Data: Record<string, unknown>;
|
|
100
|
+
let arm64Data: Record<string, unknown>;
|
|
101
|
+
|
|
102
|
+
beforeAll(async () => {
|
|
103
|
+
x86Data = await Bun.file("fixtures/deep-inspect.x86.sample.json").json();
|
|
104
|
+
arm64Data = await Bun.file("fixtures/deep-inspect.arm64.sample.json").json();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Tests
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
describe("parseDesc", () => {
|
|
112
|
+
test("string value", () => {
|
|
113
|
+
const r = parseDesc("string value");
|
|
114
|
+
expect(r.dataType).toBe("string");
|
|
115
|
+
expect(r.maxLength).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("string value, max length N", () => {
|
|
119
|
+
const r = parseDesc("string value, max length 255");
|
|
120
|
+
expect(r.dataType).toBe("string");
|
|
121
|
+
expect(r.maxLength).toBe(255);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("script", () => {
|
|
125
|
+
const r = parseDesc("script");
|
|
126
|
+
expect(r.dataType).toBe("script");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("time interval", () => {
|
|
130
|
+
const r = parseDesc("time interval");
|
|
131
|
+
expect(r.dataType).toBe("time");
|
|
132
|
+
expect(r.rangeMin).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("time interval with range", () => {
|
|
136
|
+
const r = parseDesc("00:00:00.100..00:30:00 (time interval)");
|
|
137
|
+
expect(r.dataType).toBe("time");
|
|
138
|
+
expect(r.rangeMin).toBe("00:00:00.100");
|
|
139
|
+
expect(r.rangeMax).toBe("00:30:00");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("simple enum", () => {
|
|
143
|
+
const r = parseDesc("auto|disabled|enabled");
|
|
144
|
+
expect(r.dataType).toBe("enum");
|
|
145
|
+
expect(r.enumValues).not.toBeNull();
|
|
146
|
+
expect(JSON.parse(r.enumValues as string)).toEqual(["auto", "disabled", "enabled"]);
|
|
147
|
+
expect(r.enumMulti).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("enum with multi marker and type tag", () => {
|
|
151
|
+
const r = parseDesc("ftp|reboot|read|write|policy|test|password|sniff|sensitive|romon[,Permission*]");
|
|
152
|
+
expect(r.dataType).toBe("enum");
|
|
153
|
+
expect(r.enumMulti).toBe(1);
|
|
154
|
+
expect(r.typeTag).toBe("Permission");
|
|
155
|
+
expect(r.enumValues).not.toBeNull();
|
|
156
|
+
const vals = JSON.parse(r.enumValues as string);
|
|
157
|
+
expect(vals).toContain("ftp");
|
|
158
|
+
expect(vals).toContain("romon");
|
|
159
|
+
expect(vals.length).toBe(10);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("null desc returns all nulls", () => {
|
|
163
|
+
const r = parseDesc(null);
|
|
164
|
+
expect(r.dataType).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("unknown desc returns all nulls", () => {
|
|
168
|
+
const r = parseDesc("something custom");
|
|
169
|
+
expect(r.dataType).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("integer range", () => {
|
|
173
|
+
const r = parseDesc("0..4294967295 (integer)");
|
|
174
|
+
expect(r.dataType).toBe("integer");
|
|
175
|
+
expect(r.rangeMin).toBe("0");
|
|
176
|
+
expect(r.rangeMax).toBe("4294967295");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("fixture import", () => {
|
|
181
|
+
test("walk produces expected node counts", () => {
|
|
182
|
+
const x86Nodes: FlatNode[] = [];
|
|
183
|
+
const arm64Nodes: FlatNode[] = [];
|
|
184
|
+
walk(x86Data, "", x86Nodes);
|
|
185
|
+
walk(arm64Data, "", arm64Nodes);
|
|
186
|
+
|
|
187
|
+
// x86 has /system/check-disk, /system/console, /system/console/screen (3 extra)
|
|
188
|
+
// arm64 has /interface/wifi-qcom, wifi-qcom/info, wifi-qcom/info/interface (3 extra)
|
|
189
|
+
// Both have /app/add and /app/add/copy-from
|
|
190
|
+
expect(x86Nodes.length).toBe(33);
|
|
191
|
+
expect(arm64Nodes.length).toBe(33);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("arch diff detection", () => {
|
|
195
|
+
const x86Nodes: FlatNode[] = [];
|
|
196
|
+
const arm64Nodes: FlatNode[] = [];
|
|
197
|
+
walk(x86Data, "", x86Nodes);
|
|
198
|
+
walk(arm64Data, "", arm64Nodes);
|
|
199
|
+
|
|
200
|
+
const merged = mergeArchNodes(x86Nodes, arm64Nodes);
|
|
201
|
+
|
|
202
|
+
const x86Only = merged.filter((n) => n.arch === "x86");
|
|
203
|
+
const arm64Only = merged.filter((n) => n.arch === "arm64");
|
|
204
|
+
const shared = merged.filter((n) => n.arch === null);
|
|
205
|
+
|
|
206
|
+
expect(x86Only.length).toBe(3);
|
|
207
|
+
expect(arm64Only.length).toBe(3);
|
|
208
|
+
expect(shared.length).toBe(30);
|
|
209
|
+
expect(merged.length).toBe(36);
|
|
210
|
+
|
|
211
|
+
// Verify specific arch-only paths
|
|
212
|
+
const x86Paths = x86Only.map((n) => n.path).sort();
|
|
213
|
+
expect(x86Paths).toContain("/system/check-disk");
|
|
214
|
+
expect(x86Paths).toContain("/system/console");
|
|
215
|
+
expect(x86Paths).toContain("/system/console/screen");
|
|
216
|
+
|
|
217
|
+
const arm64Paths = arm64Only.map((n) => n.path).sort();
|
|
218
|
+
expect(arm64Paths).toContain("/interface/wifi-qcom");
|
|
219
|
+
expect(arm64Paths).toContain("/interface/wifi-qcom/info");
|
|
220
|
+
expect(arm64Paths).toContain("/interface/wifi-qcom/info/interface");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("completion data preserved in merge", () => {
|
|
224
|
+
const x86Nodes: FlatNode[] = [];
|
|
225
|
+
const arm64Nodes: FlatNode[] = [];
|
|
226
|
+
walk(x86Data, "", x86Nodes);
|
|
227
|
+
walk(arm64Data, "", arm64Nodes);
|
|
228
|
+
|
|
229
|
+
const merged = mergeArchNodes(x86Nodes, arm64Nodes);
|
|
230
|
+
const withCompletion = merged.filter((n) => n.completion !== null);
|
|
231
|
+
expect(withCompletion.length).toBe(4);
|
|
232
|
+
|
|
233
|
+
// Check disabled arg completion shape
|
|
234
|
+
const disabled = withCompletion.find((n) => n.path === "/ip/address/add/disabled");
|
|
235
|
+
expect(disabled).toBeDefined();
|
|
236
|
+
expect(disabled?.completion).toHaveProperty("no");
|
|
237
|
+
expect(disabled?.completion).toHaveProperty("yes");
|
|
238
|
+
expect(disabled?.completion?.no.style).toBe("arg");
|
|
239
|
+
|
|
240
|
+
// Check copy-from with desc
|
|
241
|
+
const copyFrom = withCompletion.find((n) => n.path === "/app/add/copy-from");
|
|
242
|
+
expect(copyFrom).toBeDefined();
|
|
243
|
+
expect(copyFrom?.completion?.["my-app"].desc).toBe("My custom app");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("full import into in-memory DB via importSchemaNodes", () => {
|
|
247
|
+
const testDb = createTestDb();
|
|
248
|
+
const x86Nodes: FlatNode[] = [];
|
|
249
|
+
const arm64Nodes: FlatNode[] = [];
|
|
250
|
+
walk(x86Data, "", x86Nodes);
|
|
251
|
+
walk(arm64Data, "", arm64Nodes);
|
|
252
|
+
const merged = mergeArchNodes(x86Nodes, arm64Nodes);
|
|
253
|
+
|
|
254
|
+
importSchemaNodes(testDb, merged, "7.99-fixture", {
|
|
255
|
+
accumulate: false,
|
|
256
|
+
extraPackages: false,
|
|
257
|
+
channel: "stable",
|
|
258
|
+
x86Source: "fixtures/deep-inspect.x86.sample.json",
|
|
259
|
+
arm64Source: "fixtures/deep-inspect.arm64.sample.json",
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Verify counts
|
|
263
|
+
const count = (sql: string) => (testDb.prepare(sql).get() as { c: number }).c;
|
|
264
|
+
expect(count("SELECT COUNT(*) as c FROM schema_nodes")).toBe(36);
|
|
265
|
+
expect(count("SELECT COUNT(*) as c FROM schema_nodes WHERE _arch IS NULL")).toBe(30);
|
|
266
|
+
expect(count("SELECT COUNT(*) as c FROM schema_nodes WHERE _arch = 'x86'")).toBe(3);
|
|
267
|
+
expect(count("SELECT COUNT(*) as c FROM schema_nodes WHERE _arch = 'arm64'")).toBe(3);
|
|
268
|
+
expect(count("SELECT COUNT(*) as c FROM schema_nodes WHERE _attrs IS NOT NULL")).toBe(4);
|
|
269
|
+
|
|
270
|
+
// Verify dir_role
|
|
271
|
+
const ipAddr = testDb.prepare("SELECT dir_role FROM schema_nodes WHERE path = '/ip/address'").get() as { dir_role: string };
|
|
272
|
+
expect(ipAddr.dir_role).toBe("list"); // has cmds: add, set, remove, print
|
|
273
|
+
|
|
274
|
+
const ip = testDb.prepare("SELECT dir_role FROM schema_nodes WHERE path = '/ip'").get() as { dir_role: string };
|
|
275
|
+
expect(ip.dir_role).toBe("namespace"); // only has dir children
|
|
276
|
+
|
|
277
|
+
const system = testDb.prepare("SELECT dir_role FROM schema_nodes WHERE path = '/system'").get() as { dir_role: string };
|
|
278
|
+
// /system has both dirs (console, script) and cmds (shutdown, check-disk)
|
|
279
|
+
expect(system.dir_role).toBe("hybrid");
|
|
280
|
+
|
|
281
|
+
// Verify desc parsing
|
|
282
|
+
const comment = testDb.prepare("SELECT data_type, max_length FROM schema_nodes WHERE path = '/ip/address/add/comment'").get() as { data_type: string; max_length: number };
|
|
283
|
+
expect(comment.data_type).toBe("string");
|
|
284
|
+
expect(comment.max_length).toBe(255);
|
|
285
|
+
|
|
286
|
+
const policy = testDb.prepare("SELECT data_type, enum_values, enum_multi, type_tag FROM schema_nodes WHERE path = '/system/script/add/policy'").get() as { data_type: string; enum_values: string; enum_multi: number; type_tag: string };
|
|
287
|
+
expect(policy.data_type).toBe("enum");
|
|
288
|
+
expect(policy.enum_multi).toBe(1);
|
|
289
|
+
expect(policy.type_tag).toBe("Permission");
|
|
290
|
+
expect(JSON.parse(policy.enum_values)).toContain("ftp");
|
|
291
|
+
|
|
292
|
+
const interval = testDb.prepare("SELECT data_type, range_min, range_max FROM schema_nodes WHERE path = '/interface/monitor/interval'").get() as { data_type: string; range_min: string; range_max: string };
|
|
293
|
+
expect(interval.data_type).toBe("time");
|
|
294
|
+
expect(interval.range_min).toBe("00:00:00.100");
|
|
295
|
+
expect(interval.range_max).toBe("00:30:00");
|
|
296
|
+
|
|
297
|
+
const source = testDb.prepare("SELECT data_type FROM schema_nodes WHERE path = '/system/script/add/source'").get() as { data_type: string };
|
|
298
|
+
expect(source.data_type).toBe("script");
|
|
299
|
+
|
|
300
|
+
// Verify completion round-trip
|
|
301
|
+
const disabled = testDb.prepare("SELECT _attrs FROM schema_nodes WHERE path = '/ip/address/add/disabled'").get() as { _attrs: string };
|
|
302
|
+
const attrs = JSON.parse(disabled._attrs);
|
|
303
|
+
expect(attrs.completion).toBeDefined();
|
|
304
|
+
expect(attrs.completion.no.style).toBe("arg");
|
|
305
|
+
expect(attrs.completion.yes.preference).toBe(96);
|
|
306
|
+
|
|
307
|
+
const copyFrom = testDb.prepare("SELECT _attrs FROM schema_nodes WHERE path = '/app/add/copy-from'").get() as { _attrs: string };
|
|
308
|
+
const cfAttrs = JSON.parse(copyFrom._attrs);
|
|
309
|
+
expect(cfAttrs.completion["my-app"].desc).toBe("My custom app");
|
|
310
|
+
expect(cfAttrs.completion["dns-doh"].desc).toBe("DNS over HTTPS");
|
|
311
|
+
|
|
312
|
+
// Verify legacy compat: commands table regenerated
|
|
313
|
+
expect(count("SELECT COUNT(*) as c FROM commands")).toBe(36);
|
|
314
|
+
|
|
315
|
+
// Verify schema_node_presence
|
|
316
|
+
expect(count("SELECT COUNT(*) as c FROM schema_node_presence")).toBe(36);
|
|
317
|
+
|
|
318
|
+
// Verify command_versions compat
|
|
319
|
+
expect(count("SELECT COUNT(*) as c FROM command_versions")).toBe(36);
|
|
320
|
+
|
|
321
|
+
// Verify parent_id self-join: dir → dir
|
|
322
|
+
const ipAddrParent = testDb.prepare(`
|
|
323
|
+
SELECT p.path AS parent FROM schema_nodes c
|
|
324
|
+
JOIN schema_nodes p ON p.id = c.parent_id
|
|
325
|
+
WHERE c.path = '/ip/address'
|
|
326
|
+
`).get() as { parent: string } | null;
|
|
327
|
+
expect(ipAddrParent?.parent).toBe("/ip");
|
|
328
|
+
|
|
329
|
+
// Verify parent_id self-join: arg → cmd (the fix — previously type='dir' filter broke this)
|
|
330
|
+
const disabledParent = testDb.prepare(`
|
|
331
|
+
SELECT p.path AS parent, p.type AS parent_type FROM schema_nodes c
|
|
332
|
+
JOIN schema_nodes p ON p.id = c.parent_id
|
|
333
|
+
WHERE c.path = '/ip/address/add/disabled'
|
|
334
|
+
`).get() as { parent: string; parent_type: string } | null;
|
|
335
|
+
expect(disabledParent?.parent).toBe("/ip/address/add");
|
|
336
|
+
expect(disabledParent?.parent_type).toBe("cmd");
|
|
337
|
+
|
|
338
|
+
testDb.close();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("schema_node_presence", () => {
|
|
343
|
+
test("presence populated for all nodes via importSchemaNodes", () => {
|
|
344
|
+
const testDb = createTestDb();
|
|
345
|
+
const x86Nodes: FlatNode[] = [];
|
|
346
|
+
const arm64Nodes: FlatNode[] = [];
|
|
347
|
+
walk(x86Data, "", x86Nodes);
|
|
348
|
+
walk(arm64Data, "", arm64Nodes);
|
|
349
|
+
const merged = mergeArchNodes(x86Nodes, arm64Nodes);
|
|
350
|
+
|
|
351
|
+
importSchemaNodes(testDb, merged, "7.99-fixture", {
|
|
352
|
+
accumulate: false,
|
|
353
|
+
extraPackages: false,
|
|
354
|
+
channel: "stable",
|
|
355
|
+
x86Source: "fixtures/deep-inspect.x86.sample.json",
|
|
356
|
+
arm64Source: "fixtures/deep-inspect.arm64.sample.json",
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const count = (testDb.prepare("SELECT COUNT(*) as c FROM schema_node_presence").get() as { c: number }).c;
|
|
360
|
+
expect(count).toBe(36);
|
|
361
|
+
|
|
362
|
+
// Verify specific node presence
|
|
363
|
+
const wifiQcom = testDb.prepare(`
|
|
364
|
+
SELECT snp.version FROM schema_node_presence snp
|
|
365
|
+
JOIN schema_nodes sn ON sn.id = snp.node_id
|
|
366
|
+
WHERE sn.path = '/interface/wifi-qcom'
|
|
367
|
+
`).all() as Array<{ version: string }>;
|
|
368
|
+
expect(wifiQcom.length).toBe(1);
|
|
369
|
+
expect(wifiQcom[0].version).toBe("7.99-fixture");
|
|
370
|
+
|
|
371
|
+
testDb.close();
|
|
372
|
+
});
|
|
373
|
+
});
|