@tikoci/rosetta 0.4.1 → 0.4.3

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/src/paths.ts CHANGED
@@ -73,6 +73,14 @@ export function detectMode(srcDir: string): InvocationMode {
73
73
  return "package";
74
74
  }
75
75
 
76
+ /**
77
+ * Schema version for ros-help.db.
78
+ * Increment when making destructive schema changes (DROP/RENAME table or column).
79
+ * Stamped into the DB via `PRAGMA user_version` by initDb() and checked at MCP
80
+ * startup to detect stale DBs for bunx users who auto-update the package.
81
+ */
82
+ export const SCHEMA_VERSION = 1;
83
+
76
84
  /**
77
85
  * Resolve the version string.
78
86
  * Compiled mode: injected at build time via --define.
package/src/query.test.ts CHANGED
@@ -13,10 +13,12 @@ import { beforeAll, describe, expect, test } from "bun:test";
13
13
  process.env.DB_PATH = ":memory:";
14
14
 
15
15
  // Dynamic imports so the env-var assignment above is visible to db.ts
16
- const { db, initDb, getDbStats } = await import("./db.ts");
16
+ const { db, initDb, getDbStats, checkSchemaVersion, SCHEMA_VERSION } = await import("./db.ts");
17
17
  const {
18
18
  extractTerms,
19
19
  buildFtsQuery,
20
+ exportDevicesCsv,
21
+ exportDeviceTestsCsv,
20
22
  searchPages,
21
23
  getPage,
22
24
  lookupProperty,
@@ -24,9 +26,11 @@ const {
24
26
  searchCallouts,
25
27
  searchChangelogs,
26
28
  checkCommandVersions,
29
+ diffCommandVersions,
27
30
  searchDevices,
28
31
  searchDeviceTests,
29
32
  getTestResultMeta,
33
+ normalizeDeviceQuery,
30
34
  } = await import("./query.ts");
31
35
  const { parseChangelog } = await import("./extract-changelogs.ts");
32
36
 
@@ -89,6 +93,17 @@ beforeAll(() => {
89
93
  db.run(`INSERT INTO command_versions (command_path, ros_version)
90
94
  VALUES ('/ip/dhcp-server', '7.9')`);
91
95
 
96
+ // Extra command_versions entries to support diffCommandVersions tests
97
+ // /ip/dhcp-server/lease only in 7.22 (added)
98
+ db.run(`INSERT INTO command_versions (command_path, ros_version)
99
+ VALUES ('/ip/dhcp-server/lease', '7.22')`);
100
+ // /ip/old-feature only in 7.9 (removed by 7.22)
101
+ db.run(`INSERT INTO command_versions (command_path, ros_version)
102
+ VALUES ('/ip/old-feature', '7.9')`);
103
+ // /other/path only in 7.10.2 (outside /ip prefix for prefix-filter test)
104
+ db.run(`INSERT INTO command_versions (command_path, ros_version)
105
+ VALUES ('/other/path', '7.10.2')`);
106
+
92
107
  // Device fixtures for searchDevices tests
93
108
  db.run(`INSERT INTO devices
94
109
  (product_name, product_code, architecture, cpu, cpu_cores, cpu_frequency,
@@ -171,6 +186,75 @@ beforeAll(() => {
171
186
  NULL, 13, NULL, NULL, NULL,
172
187
  NULL, NULL, NULL, 369.00)`);
173
188
 
189
+ // Device fixtures: Unicode superscript names (matching production DB naming)
190
+ db.run(`INSERT INTO devices
191
+ (product_name, product_code, architecture, cpu, cpu_cores, cpu_frequency,
192
+ license_level, operating_system, ram, ram_mb, storage, storage_mb,
193
+ poe_in, poe_out, wireless_24_chains, wireless_5_chains,
194
+ eth_fast, eth_gigabit, eth_2500, sfp_ports, sfp_plus_ports,
195
+ eth_multigig, usb_ports, sim_slots, msrp_usd,
196
+ product_url)
197
+ VALUES
198
+ ('hAP ax\u00b2', 'C52iG-5HaxD2HaxD-TC', 'ARM 64bit', 'IPQ-6010', 4, 'auto (864 - 1800) MHz',
199
+ 4, 'RouterOS v7', '1 GB', 1024, '128 MB', 128,
200
+ '802.3af/at', NULL, 2, 2,
201
+ NULL, 4, 1, NULL, NULL,
202
+ NULL, 1, NULL, 119.00,
203
+ 'https://mikrotik.com/product/hap_ax2')`);
204
+
205
+ db.run(`INSERT INTO devices
206
+ (product_name, product_code, architecture, cpu, cpu_cores, cpu_frequency,
207
+ license_level, operating_system, ram, ram_mb, storage, storage_mb,
208
+ poe_in, poe_out, wireless_24_chains, wireless_5_chains,
209
+ eth_fast, eth_gigabit, eth_2500, sfp_ports, sfp_plus_ports,
210
+ eth_multigig, usb_ports, sim_slots, msrp_usd)
211
+ VALUES
212
+ ('hAP ac\u00b3', 'RBD53iG-5HacD2HnD', 'ARM 64bit', 'IPQ-4019', 4, '716 MHz',
213
+ 4, 'RouterOS v7', '256 MB', 256, '128 MB', 128,
214
+ NULL, NULL, 2, 2,
215
+ NULL, 5, NULL, NULL, NULL,
216
+ NULL, 1, NULL, 69.00)`);
217
+
218
+ // Device fixtures: RB5009 family for disambiguation testing
219
+ db.run(`INSERT INTO devices
220
+ (product_name, product_code, architecture, cpu, cpu_cores, cpu_frequency,
221
+ license_level, operating_system, ram, ram_mb, storage, storage_mb,
222
+ poe_in, poe_out, wireless_24_chains, wireless_5_chains,
223
+ eth_fast, eth_gigabit, eth_2500, sfp_ports, sfp_plus_ports,
224
+ eth_multigig, usb_ports, sim_slots, msrp_usd)
225
+ VALUES
226
+ ('RB5009UG+S+IN', 'RB5009UG+S+IN', 'ARM 64bit', 'Marvell 88F7040', 4, '1400 MHz',
227
+ 5, 'RouterOS v7', '1 GB', 1024, '1 GB', 1024,
228
+ NULL, NULL, NULL, NULL,
229
+ NULL, 7, 1, NULL, 1,
230
+ NULL, 1, NULL, 219.00)`);
231
+
232
+ db.run(`INSERT INTO devices
233
+ (product_name, product_code, architecture, cpu, cpu_cores, cpu_frequency,
234
+ license_level, operating_system, ram, ram_mb, storage, storage_mb,
235
+ poe_in, poe_out, wireless_24_chains, wireless_5_chains,
236
+ eth_fast, eth_gigabit, eth_2500, sfp_ports, sfp_plus_ports,
237
+ eth_multigig, usb_ports, sim_slots, msrp_usd)
238
+ VALUES
239
+ ('RB5009UPr+S+IN', 'RB5009UPr+S+IN', 'ARM 64bit', 'Marvell 88F7040', 4, '1400 MHz',
240
+ 5, 'RouterOS v7', '1 GB', 1024, '1 GB', 1024,
241
+ '802.3af/at', '802.3af/at', NULL, NULL,
242
+ NULL, 7, 1, NULL, 1,
243
+ NULL, 1, NULL, 269.00)`);
244
+
245
+ db.run(`INSERT INTO devices
246
+ (product_name, product_code, architecture, cpu, cpu_cores, cpu_frequency,
247
+ license_level, operating_system, ram, ram_mb, storage, storage_mb,
248
+ poe_in, poe_out, wireless_24_chains, wireless_5_chains,
249
+ eth_fast, eth_gigabit, eth_2500, sfp_ports, sfp_plus_ports,
250
+ eth_multigig, usb_ports, sim_slots, msrp_usd)
251
+ VALUES
252
+ ('RB5009UPr+S+OUT', 'RB5009UPr+S+OUT', 'ARM 64bit', 'Marvell 88F7040', 4, '1400 MHz',
253
+ 5, 'RouterOS v7', '1 GB', 1024, '1 GB', 1024,
254
+ '802.3af/at', '802.3af/at', NULL, NULL,
255
+ NULL, 7, 1, NULL, 1,
256
+ NULL, 1, NULL, 299.00)`);
257
+
174
258
  // Device test results fixtures (hAP ax3 = id 1)
175
259
  db.run(`INSERT INTO device_test_results
176
260
  (device_id, test_type, mode, configuration, packet_size, throughput_kpps, throughput_mbps)
@@ -668,6 +752,79 @@ describe("checkCommandVersions", () => {
668
752
  });
669
753
  });
670
754
 
755
+ // ---------------------------------------------------------------------------
756
+ // DB integration: diffCommandVersions
757
+ // ---------------------------------------------------------------------------
758
+
759
+ describe("diffCommandVersions", () => {
760
+ test("detects added command paths", () => {
761
+ // /ip/dhcp-server/lease only in 7.22
762
+ const res = diffCommandVersions("7.9", "7.22");
763
+ expect(res.added).toContain("/ip/dhcp-server/lease");
764
+ });
765
+
766
+ test("detects removed command paths", () => {
767
+ // /ip/old-feature only in 7.9
768
+ const res = diffCommandVersions("7.9", "7.22");
769
+ expect(res.removed).toContain("/ip/old-feature");
770
+ });
771
+
772
+ test("does not report unchanged commands as added or removed", () => {
773
+ const res = diffCommandVersions("7.9", "7.22");
774
+ // /ip/dhcp-server is in both versions — should not appear in either list
775
+ expect(res.added).not.toContain("/ip/dhcp-server");
776
+ expect(res.removed).not.toContain("/ip/dhcp-server");
777
+ });
778
+
779
+ test("returns correct counts", () => {
780
+ const res = diffCommandVersions("7.9", "7.22");
781
+ expect(res.added_count).toBe(res.added.length);
782
+ expect(res.removed_count).toBe(res.removed.length);
783
+ });
784
+
785
+ test("path_prefix scopes the diff to a subtree", () => {
786
+ const res = diffCommandVersions("7.9", "7.22", "/ip");
787
+ // /other/path is outside /ip prefix — should not appear
788
+ expect(res.added).not.toContain("/other/path");
789
+ expect(res.removed).not.toContain("/other/path");
790
+ // Results should still include /ip subtree changes
791
+ expect(res.removed).toContain("/ip/old-feature");
792
+ });
793
+
794
+ test("returns from_version and to_version in result", () => {
795
+ const res = diffCommandVersions("7.9", "7.22");
796
+ expect(res.from_version).toBe("7.9");
797
+ expect(res.to_version).toBe("7.22");
798
+ });
799
+
800
+ test("returns path_prefix in result", () => {
801
+ const res = diffCommandVersions("7.9", "7.22", "/ip/firewall");
802
+ expect(res.path_prefix).toBe("/ip/firewall");
803
+ });
804
+
805
+ test("path_prefix null when not provided", () => {
806
+ const res = diffCommandVersions("7.9", "7.22");
807
+ expect(res.path_prefix).toBeNull();
808
+ });
809
+
810
+ test("adds note for untracked from_version", () => {
811
+ const res = diffCommandVersions("7.1", "7.22");
812
+ expect(res.note).toContain("7.1");
813
+ expect(res.note).toContain("not in the tracked range");
814
+ });
815
+
816
+ test("adds note for untracked to_version", () => {
817
+ const res = diffCommandVersions("7.9", "7.99");
818
+ expect(res.note).toContain("7.99");
819
+ });
820
+
821
+ test("returns empty diff for same version", () => {
822
+ const res = diffCommandVersions("7.22", "7.22");
823
+ expect(res.added).toHaveLength(0);
824
+ expect(res.removed).toHaveLength(0);
825
+ });
826
+ });
827
+
671
828
  // ---------------------------------------------------------------------------
672
829
  // DB integration: searchDevices
673
830
  // ---------------------------------------------------------------------------
@@ -709,14 +866,14 @@ describe("searchDevices", () => {
709
866
  test("filter by architecture", () => {
710
867
  const res = searchDevices("", { architecture: "ARM 64bit" });
711
868
  expect(res.mode).toBe("filter");
712
- expect(res.results.length).toBe(3);
869
+ expect(res.results.length).toBe(8); // hAP ax3 + CCR2216 + Chateau + hAP ax² + hAP ac³ + 3× RB5009
713
870
  expect(res.results.every((d) => d.architecture === "ARM 64bit")).toBe(true);
714
871
  });
715
872
 
716
873
  test("filter by min_ram_mb", () => {
717
874
  const res = searchDevices("", { min_ram_mb: 1024 });
718
875
  expect(res.mode).toBe("filter");
719
- expect(res.results.length).toBe(5); // hAP ax3 (1024) + CCR2216 (16384) + Chateau (1024) + RB1100AHx4 (1024) + RB1100AHx4 Dude (1024)
876
+ expect(res.results.length).toBe(9); // +hAP ax²(1024) +3×RB5009(1024) not hAP ac³(256)
720
877
  expect(res.results.every((d) => (d.ram_mb ?? 0) >= 1024)).toBe(true);
721
878
  });
722
879
 
@@ -729,19 +886,19 @@ describe("searchDevices", () => {
729
886
 
730
887
  test("filter by has_poe", () => {
731
888
  const res = searchDevices("", { has_poe: true });
732
- expect(res.results).toHaveLength(1);
733
- expect(res.results[0].product_name).toBe("hAP ax3");
889
+ expect(res.results).toHaveLength(4); // hAP ax3 + hAP ax² + RB5009UPr IN + RB5009UPr OUT
890
+ expect(res.results.every((d) => d.poe_in != null || d.poe_out != null)).toBe(true);
734
891
  });
735
892
 
736
893
  test("filter by has_wireless", () => {
737
894
  const res = searchDevices("", { has_wireless: true });
738
- expect(res.results).toHaveLength(3); // hAP ax3 + hAP lite + Chateau LTE18
895
+ expect(res.results).toHaveLength(5); // hAP ax3 + hAP lite + Chateau LTE18 + hAP ax² + hAP ac³
739
896
  });
740
897
 
741
898
  test("filter by min_storage_mb", () => {
742
899
  const res = searchDevices("", { min_storage_mb: 128 });
743
900
  expect(res.mode).toBe("filter");
744
- expect(res.results.length).toBe(5); // hAP ax3 (128) + CCR2216 (128) + Chateau (128) + RB1100AHx4 (128) + RB1100AHx4 Dude (512)
901
+ expect(res.results.length).toBe(10); // +hAP ax²(128) +hAP ac³(128) +3×RB5009(1024)
745
902
  expect(res.results.every((d) => (d.storage_mb ?? 0) >= 128)).toBe(true);
746
903
  });
747
904
 
@@ -810,10 +967,11 @@ describe("searchDevices", () => {
810
967
  expect(res.mode).toBe("exact");
811
968
  expect(res.results).toHaveLength(1);
812
969
  const dev = res.results[0];
813
- expect(dev.test_results).toBeDefined();
814
- expect(dev.test_results!.length).toBe(3);
815
- expect(dev.test_results!.some((t) => t.test_type === "ethernet")).toBe(true);
816
- expect(dev.test_results!.some((t) => t.test_type === "ipsec")).toBe(true);
970
+ const testResults = dev.test_results;
971
+ expect(testResults).toBeDefined();
972
+ expect(testResults?.length).toBe(3);
973
+ expect(testResults?.some((t) => t.test_type === "ethernet")).toBe(true);
974
+ expect(testResults?.some((t) => t.test_type === "ipsec")).toBe(true);
817
975
  });
818
976
 
819
977
  test("LIKE match with ≤5 results includes test_results", () => {
@@ -855,6 +1013,112 @@ describe("searchDevices", () => {
855
1013
  expect(res.results[0].test_results).toBeDefined();
856
1014
  }
857
1015
  });
1016
+
1017
+ test("LIKE splits on dashes so rb1100-ahx4 finds RB1100AHx4 family via LIKE", () => {
1018
+ // Users may type model numbers with dashes as word separators
1019
+ const res = searchDevices("RB1100-AHx4");
1020
+ expect(res.mode).toBe("like");
1021
+ expect(res.results.length).toBeGreaterThanOrEqual(1);
1022
+ expect(res.results.every((d) => d.product_name.includes("RB1100"))).toBe(true);
1023
+ });
1024
+
1025
+ test("slug-normalized LIKE finds hapax3 → hAP ax3 via product_url", () => {
1026
+ // Concatenated slug-style query: spaces dropped, ASCII digit for superscript.
1027
+ // Falls through regular LIKE (no match: 'hapax3' not a substring of 'hAP ax3')
1028
+ // then slug-normalized path matches product_url /product/hap_ax3 → hap_ax3 stripped.
1029
+ const res = searchDevices("hapax3");
1030
+ expect(res.results).toHaveLength(1);
1031
+ expect(res.results[0].product_name).toBe("hAP ax3"); // fixture uses ASCII 3
1032
+ });
1033
+
1034
+ test("dash-split LIKE finds hap-ax3 → hAP ax3", () => {
1035
+ // Dash as separator: split → ['hap','ax3'] → LIKE '%hap%' AND '%ax3%'
1036
+ const res = searchDevices("hap-ax3");
1037
+ expect(res.mode).toBe("like");
1038
+ expect(res.results).toHaveLength(1);
1039
+ expect(res.results[0].product_name).toBe("hAP ax3");
1040
+ });
1041
+
1042
+ test("underscore-split LIKE finds hap_ax3 → hAP ax3", () => {
1043
+ // Underscore-separated slug form
1044
+ const res = searchDevices("hap_ax3");
1045
+ expect(res.mode).toBe("like");
1046
+ expect(res.results).toHaveLength(1);
1047
+ expect(res.results[0].product_name).toBe("hAP ax3");
1048
+ });
1049
+
1050
+ // ── Unicode superscript normalization ──
1051
+
1052
+ test("normalizeDeviceQuery converts superscripts to ASCII", () => {
1053
+ expect(normalizeDeviceQuery("hAP ax³")).toBe("hAP ax3");
1054
+ expect(normalizeDeviceQuery("hAP ax²")).toBe("hAP ax2");
1055
+ expect(normalizeDeviceQuery("hAP ac³")).toBe("hAP ac3");
1056
+ expect(normalizeDeviceQuery("no superscripts")).toBe("no superscripts");
1057
+ });
1058
+
1059
+ test("exact match with Unicode query hAP ax³ finds ASCII-named hAP ax3", () => {
1060
+ // User pastes Unicode name, DB has ASCII variant
1061
+ const res = searchDevices("hAP ax\u00B3");
1062
+ expect(res.mode).toBe("exact");
1063
+ expect(res.results).toHaveLength(1);
1064
+ expect(res.results[0].product_name).toBe("hAP ax3");
1065
+ });
1066
+
1067
+ test("ASCII query hap ax2 finds Unicode-named hAP ax²", () => {
1068
+ // User types ASCII digits, DB has Unicode superscript
1069
+ const res = searchDevices("hap ax2");
1070
+ expect(res.mode).toBe("exact");
1071
+ expect(res.results).toHaveLength(1);
1072
+ expect(res.results[0].product_name).toBe("hAP ax\u00B2");
1073
+ });
1074
+
1075
+ test("ASCII query hap ac3 finds Unicode-named hAP ac³", () => {
1076
+ const res = searchDevices("hap ac3");
1077
+ expect(res.mode).toBe("exact");
1078
+ expect(res.results).toHaveLength(1);
1079
+ expect(res.results[0].product_name).toBe("hAP ac\u00B3");
1080
+ });
1081
+
1082
+ test("single-digit term preserved: hap ax 3 finds hAP ax3 (not 4 results)", () => {
1083
+ // Previously: digit '3' was filtered by length >= 2, leaving just 'hap' + 'ax'
1084
+ // which matched hAP ax S, hAP ax lite, hAP ax², hAP ax³ (broad).
1085
+ // Now: single digits kept when accompanied by longer terms.
1086
+ const res = searchDevices("hap ax 3");
1087
+ expect(res.results).toHaveLength(1);
1088
+ expect(res.results[0].product_name).toBe("hAP ax3");
1089
+ });
1090
+
1091
+ // ── Multi-match disambiguation ──
1092
+
1093
+ test("RB5009 family query returns all 3 variants with disambiguation note", () => {
1094
+ const res = searchDevices("RB5009");
1095
+ expect(res.mode).toBe("like");
1096
+ expect(res.results).toHaveLength(3);
1097
+ expect(res.results.every((d) => d.product_name.includes("RB5009"))).toBe(true);
1098
+ // Should include disambiguation note for multi-match
1099
+ expect(res.note).toBeDefined();
1100
+ expect(res.note).toContain("3 devices");
1101
+ });
1102
+
1103
+ test("disambiguation note mentions PoE difference for RB5009 family", () => {
1104
+ const res = searchDevices("RB5009");
1105
+ expect(res.note).toBeDefined();
1106
+ // RB5009UG has no PoE, RB5009UPr has PoE → note should mention it
1107
+ expect(res.note).toContain("PoE");
1108
+ });
1109
+
1110
+ test("disambiguation note mentions enclosure difference for RB5009 family", () => {
1111
+ const res = searchDevices("RB5009");
1112
+ expect(res.note).toBeDefined();
1113
+ // IN vs OUT enclosures
1114
+ expect(res.note).toContain("enclosure");
1115
+ });
1116
+
1117
+ test("single LIKE match has no disambiguation note", () => {
1118
+ const res = searchDevices("CCR2216");
1119
+ expect(res.results).toHaveLength(1);
1120
+ expect(res.note).toBeUndefined();
1121
+ });
858
1122
  });
859
1123
 
860
1124
  // ---------------------------------------------------------------------------
@@ -899,10 +1163,22 @@ describe("searchDeviceTests", () => {
899
1163
  }
900
1164
  });
901
1165
 
902
- test("includes device info in results", () => {
1166
+ test("result shape has expected fields and no cpu fields", () => {
903
1167
  const res = searchDeviceTests({ test_type: "ethernet" });
904
- expect(res.results[0].product_name).toBeDefined();
905
- expect(res.results[0].architecture).toBeDefined();
1168
+ const row = res.results[0];
1169
+ // Present: device identity + test data
1170
+ expect(row.product_name).toBeDefined();
1171
+ expect(row.product_code).toBeDefined();
1172
+ expect(row.architecture).toBeDefined();
1173
+ expect(row.test_type).toBeDefined();
1174
+ expect(row.mode).toBeDefined();
1175
+ expect(row.configuration).toBeDefined();
1176
+ expect(row.packet_size).toBeDefined();
1177
+ expect(row.throughput_mbps).toBeDefined();
1178
+ // Absent: cpu details (available via device_lookup)
1179
+ expect("cpu" in row).toBe(false);
1180
+ expect("cpu_cores" in row).toBe(false);
1181
+ expect("cpu_frequency" in row).toBe(false);
906
1182
  });
907
1183
 
908
1184
  test("respects limit", () => {
@@ -912,6 +1188,30 @@ describe("searchDeviceTests", () => {
912
1188
  });
913
1189
  });
914
1190
 
1191
+ describe("dataset CSV exports", () => {
1192
+ test("exports full device test results as CSV", () => {
1193
+ const csv = exportDeviceTestsCsv();
1194
+ const lines = csv.trim().split("\n");
1195
+
1196
+ expect(lines[0]).toBe("product_name,product_code,architecture,cpu,cpu_cores,cpu_frequency,test_type,mode,configuration,packet_size,throughput_kpps,throughput_mbps,product_url");
1197
+ expect(lines).toHaveLength(4);
1198
+ expect(csv).toContain("hAP ax3");
1199
+ expect(csv).toContain("IPQ-6010");
1200
+ expect(csv).toContain("https://mikrotik.com/product/hap_ax3");
1201
+ });
1202
+
1203
+ test("exports full device catalog as CSV", () => {
1204
+ const csv = exportDevicesCsv();
1205
+ const lines = csv.trim().split("\n");
1206
+
1207
+ expect(lines[0]).toBe("product_name,product_code,architecture,cpu,cpu_cores,cpu_frequency,license_level,operating_system,ram,ram_mb,storage,storage_mb,dimensions,poe_in,poe_out,max_power_w,wireless_24_chains,wireless_5_chains,eth_fast,eth_gigabit,eth_2500,sfp_ports,sfp_plus_ports,eth_multigig,usb_ports,sim_slots,msrp_usd,product_url,block_diagram_url");
1208
+ expect(lines).toHaveLength(12); // header + 11 devices (6 original + 5 new fixtures)
1209
+ expect(csv).toContain("CCR2216-1G-12XS-2XQ");
1210
+ expect(csv).toContain("https://cdn.mikrotik.com/web-assets/product_files/hap_ax3_123.png");
1211
+ expect(lines[0].startsWith("id,")).toBe(false);
1212
+ });
1213
+ });
1214
+
915
1215
  describe("getTestResultMeta", () => {
916
1216
  test("returns distinct values", () => {
917
1217
  const meta = getTestResultMeta();
@@ -1129,6 +1429,13 @@ describe("schema", () => {
1129
1429
  expect(triggers).toContain("changelogs_ad");
1130
1430
  expect(triggers).toContain("changelogs_au");
1131
1431
  });
1432
+
1433
+ test("PRAGMA user_version matches SCHEMA_VERSION", () => {
1434
+ const result = checkSchemaVersion();
1435
+ expect(result.ok).toBe(true);
1436
+ expect(result.actual).toBe(SCHEMA_VERSION);
1437
+ expect(result.expected).toBe(SCHEMA_VERSION);
1438
+ });
1132
1439
  });
1133
1440
 
1134
1441
  // ---------------------------------------------------------------------------