@tikoci/rosetta 0.4.0 → 0.4.2
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 +13 -1
- package/package.json +1 -1
- package/src/db.ts +39 -8
- package/src/extract-test-results.ts +86 -13
- package/src/mcp-http.test.ts +168 -23
- package/src/mcp.ts +241 -1
- package/src/paths.ts +8 -0
- package/src/query.test.ts +426 -13
- package/src/query.ts +420 -23
- package/src/setup.ts +7 -2
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 } = 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,7 +26,11 @@ const {
|
|
|
24
26
|
searchCallouts,
|
|
25
27
|
searchChangelogs,
|
|
26
28
|
checkCommandVersions,
|
|
29
|
+
diffCommandVersions,
|
|
27
30
|
searchDevices,
|
|
31
|
+
searchDeviceTests,
|
|
32
|
+
getTestResultMeta,
|
|
33
|
+
normalizeDeviceQuery,
|
|
28
34
|
} = await import("./query.ts");
|
|
29
35
|
const { parseChangelog } = await import("./extract-changelogs.ts");
|
|
30
36
|
|
|
@@ -69,6 +75,10 @@ beforeAll(() => {
|
|
|
69
75
|
|
|
70
76
|
db.run(`INSERT INTO ros_versions (version, channel, extra_packages, extracted_at)
|
|
71
77
|
VALUES ('7.22', 'stable', 0, '2024-01-01T00:00:00Z')`);
|
|
78
|
+
db.run(`INSERT INTO ros_versions (version, channel, extra_packages, extracted_at)
|
|
79
|
+
VALUES ('7.9', 'stable', 0, '2023-01-01T00:00:00Z')`);
|
|
80
|
+
db.run(`INSERT INTO ros_versions (version, channel, extra_packages, extracted_at)
|
|
81
|
+
VALUES ('7.10.2', 'stable', 0, '2023-06-01T00:00:00Z')`);
|
|
72
82
|
|
|
73
83
|
db.run(`INSERT INTO commands
|
|
74
84
|
(id, path, name, type, parent_path, page_id, description, ros_version)
|
|
@@ -80,6 +90,19 @@ beforeAll(() => {
|
|
|
80
90
|
|
|
81
91
|
db.run(`INSERT INTO command_versions (command_path, ros_version)
|
|
82
92
|
VALUES ('/ip/dhcp-server', '7.22')`);
|
|
93
|
+
db.run(`INSERT INTO command_versions (command_path, ros_version)
|
|
94
|
+
VALUES ('/ip/dhcp-server', '7.9')`);
|
|
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')`);
|
|
83
106
|
|
|
84
107
|
// Device fixtures for searchDevices tests
|
|
85
108
|
db.run(`INSERT INTO devices
|
|
@@ -163,6 +186,75 @@ beforeAll(() => {
|
|
|
163
186
|
NULL, 13, NULL, NULL, NULL,
|
|
164
187
|
NULL, NULL, NULL, 369.00)`);
|
|
165
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
|
+
|
|
166
258
|
// Device test results fixtures (hAP ax3 = id 1)
|
|
167
259
|
db.run(`INSERT INTO device_test_results
|
|
168
260
|
(device_id, test_type, mode, configuration, packet_size, throughput_kpps, throughput_mbps)
|
|
@@ -635,8 +727,8 @@ describe("searchCallouts", () => {
|
|
|
635
727
|
describe("checkCommandVersions", () => {
|
|
636
728
|
test("returns versions for known command path", () => {
|
|
637
729
|
const res = checkCommandVersions("/ip/dhcp-server");
|
|
638
|
-
expect(res.versions).toEqual(["7.22"]);
|
|
639
|
-
expect(res.first_seen).toBe("7.
|
|
730
|
+
expect(res.versions).toEqual(["7.9", "7.22"]);
|
|
731
|
+
expect(res.first_seen).toBe("7.9");
|
|
640
732
|
expect(res.last_seen).toBe("7.22");
|
|
641
733
|
});
|
|
642
734
|
|
|
@@ -660,6 +752,79 @@ describe("checkCommandVersions", () => {
|
|
|
660
752
|
});
|
|
661
753
|
});
|
|
662
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
|
+
|
|
663
828
|
// ---------------------------------------------------------------------------
|
|
664
829
|
// DB integration: searchDevices
|
|
665
830
|
// ---------------------------------------------------------------------------
|
|
@@ -701,14 +866,14 @@ describe("searchDevices", () => {
|
|
|
701
866
|
test("filter by architecture", () => {
|
|
702
867
|
const res = searchDevices("", { architecture: "ARM 64bit" });
|
|
703
868
|
expect(res.mode).toBe("filter");
|
|
704
|
-
expect(res.results.length).toBe(
|
|
869
|
+
expect(res.results.length).toBe(8); // hAP ax3 + CCR2216 + Chateau + hAP ax² + hAP ac³ + 3× RB5009
|
|
705
870
|
expect(res.results.every((d) => d.architecture === "ARM 64bit")).toBe(true);
|
|
706
871
|
});
|
|
707
872
|
|
|
708
873
|
test("filter by min_ram_mb", () => {
|
|
709
874
|
const res = searchDevices("", { min_ram_mb: 1024 });
|
|
710
875
|
expect(res.mode).toBe("filter");
|
|
711
|
-
expect(res.results.length).toBe(
|
|
876
|
+
expect(res.results.length).toBe(9); // +hAP ax²(1024) +3×RB5009(1024) — not hAP ac³(256)
|
|
712
877
|
expect(res.results.every((d) => (d.ram_mb ?? 0) >= 1024)).toBe(true);
|
|
713
878
|
});
|
|
714
879
|
|
|
@@ -721,19 +886,19 @@ describe("searchDevices", () => {
|
|
|
721
886
|
|
|
722
887
|
test("filter by has_poe", () => {
|
|
723
888
|
const res = searchDevices("", { has_poe: true });
|
|
724
|
-
expect(res.results).toHaveLength(
|
|
725
|
-
expect(res.results
|
|
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);
|
|
726
891
|
});
|
|
727
892
|
|
|
728
893
|
test("filter by has_wireless", () => {
|
|
729
894
|
const res = searchDevices("", { has_wireless: true });
|
|
730
|
-
expect(res.results).toHaveLength(
|
|
895
|
+
expect(res.results).toHaveLength(5); // hAP ax3 + hAP lite + Chateau LTE18 + hAP ax² + hAP ac³
|
|
731
896
|
});
|
|
732
897
|
|
|
733
898
|
test("filter by min_storage_mb", () => {
|
|
734
899
|
const res = searchDevices("", { min_storage_mb: 128 });
|
|
735
900
|
expect(res.mode).toBe("filter");
|
|
736
|
-
expect(res.results.length).toBe(
|
|
901
|
+
expect(res.results.length).toBe(10); // +hAP ax²(128) +hAP ac³(128) +3×RB5009(1024)
|
|
737
902
|
expect(res.results.every((d) => (d.storage_mb ?? 0) >= 128)).toBe(true);
|
|
738
903
|
});
|
|
739
904
|
|
|
@@ -802,10 +967,11 @@ describe("searchDevices", () => {
|
|
|
802
967
|
expect(res.mode).toBe("exact");
|
|
803
968
|
expect(res.results).toHaveLength(1);
|
|
804
969
|
const dev = res.results[0];
|
|
805
|
-
|
|
806
|
-
expect(
|
|
807
|
-
expect(
|
|
808
|
-
expect(
|
|
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);
|
|
809
975
|
});
|
|
810
976
|
|
|
811
977
|
test("LIKE match with ≤5 results includes test_results", () => {
|
|
@@ -827,6 +993,233 @@ describe("searchDevices", () => {
|
|
|
827
993
|
expect(res.results[0].product_url).toBeNull();
|
|
828
994
|
expect(res.results[0].block_diagram_url).toBeNull();
|
|
829
995
|
});
|
|
996
|
+
|
|
997
|
+
test("has_more is false when all results fit", () => {
|
|
998
|
+
const res = searchDevices("", { architecture: "ARM 64bit" }, 50);
|
|
999
|
+
expect(res.has_more).toBe(false);
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
test("has_more is true when results are truncated", () => {
|
|
1003
|
+
// We have multiple devices; limit to 1 with a broad filter
|
|
1004
|
+
const res = searchDevices("", { architecture: "ARM 64bit" }, 1);
|
|
1005
|
+
expect(res.results).toHaveLength(1);
|
|
1006
|
+
expect(res.has_more).toBe(true);
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
test("single FTS match attaches test_results", () => {
|
|
1010
|
+
// "hAP ax3" as FTS should find exactly one match and attach test results
|
|
1011
|
+
const res = searchDevices("hAP ax3");
|
|
1012
|
+
if (res.mode === "exact" || res.results.length === 1) {
|
|
1013
|
+
expect(res.results[0].test_results).toBeDefined();
|
|
1014
|
+
}
|
|
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
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
// ---------------------------------------------------------------------------
|
|
1125
|
+
// searchDeviceTests: cross-device test queries
|
|
1126
|
+
// ---------------------------------------------------------------------------
|
|
1127
|
+
|
|
1128
|
+
describe("searchDeviceTests", () => {
|
|
1129
|
+
test("returns all test results with no filters", () => {
|
|
1130
|
+
const res = searchDeviceTests({});
|
|
1131
|
+
expect(res.results.length).toBe(3);
|
|
1132
|
+
expect(res.total).toBe(3);
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
test("filters by test_type", () => {
|
|
1136
|
+
const res = searchDeviceTests({ test_type: "ethernet" });
|
|
1137
|
+
expect(res.results.every((r) => r.test_type === "ethernet")).toBe(true);
|
|
1138
|
+
expect(res.results.length).toBe(2);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
test("filters by test_type and mode", () => {
|
|
1142
|
+
const res = searchDeviceTests({ test_type: "ipsec", mode: "Single tunnel" });
|
|
1143
|
+
expect(res.results).toHaveLength(1);
|
|
1144
|
+
expect(res.results[0].configuration).toBe("AES-128-CBC + SHA1");
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
test("configuration uses LIKE matching", () => {
|
|
1148
|
+
const res = searchDeviceTests({ configuration: "25 ip filter" });
|
|
1149
|
+
expect(res.results).toHaveLength(1);
|
|
1150
|
+
expect(res.results[0].configuration).toBe("25 ip filter rules");
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
test("filters by packet_size", () => {
|
|
1154
|
+
const res = searchDeviceTests({ packet_size: 512 });
|
|
1155
|
+
expect(res.results.every((r) => r.packet_size === 512)).toBe(true);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
test("sorts by mbps descending by default", () => {
|
|
1159
|
+
const res = searchDeviceTests({ test_type: "ethernet" });
|
|
1160
|
+
if (res.results.length >= 2) {
|
|
1161
|
+
const mbps = res.results.map((r) => r.throughput_mbps ?? 0);
|
|
1162
|
+
expect(mbps[0]).toBeGreaterThanOrEqual(mbps[1]);
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
test("result shape has expected fields and no cpu fields", () => {
|
|
1167
|
+
const res = searchDeviceTests({ test_type: "ethernet" });
|
|
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);
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
test("respects limit", () => {
|
|
1185
|
+
const res = searchDeviceTests({}, 1);
|
|
1186
|
+
expect(res.results).toHaveLength(1);
|
|
1187
|
+
expect(res.total).toBe(3);
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
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
|
+
|
|
1215
|
+
describe("getTestResultMeta", () => {
|
|
1216
|
+
test("returns distinct values", () => {
|
|
1217
|
+
const meta = getTestResultMeta();
|
|
1218
|
+
expect(meta.test_types).toContain("ethernet");
|
|
1219
|
+
expect(meta.test_types).toContain("ipsec");
|
|
1220
|
+
expect(meta.modes).toContain("Routing");
|
|
1221
|
+
expect(meta.packet_sizes).toContain(512);
|
|
1222
|
+
});
|
|
830
1223
|
});
|
|
831
1224
|
|
|
832
1225
|
// ---------------------------------------------------------------------------
|
|
@@ -1036,4 +1429,24 @@ describe("schema", () => {
|
|
|
1036
1429
|
expect(triggers).toContain("changelogs_ad");
|
|
1037
1430
|
expect(triggers).toContain("changelogs_au");
|
|
1038
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
|
+
});
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
// ---------------------------------------------------------------------------
|
|
1442
|
+
// getDbStats: version range uses semantic sort (not lexicographic)
|
|
1443
|
+
// ---------------------------------------------------------------------------
|
|
1444
|
+
|
|
1445
|
+
describe("getDbStats", () => {
|
|
1446
|
+
test("version range is semantically sorted (7.9 < 7.10.2 < 7.22)", () => {
|
|
1447
|
+
const stats = getDbStats();
|
|
1448
|
+
// Fixtures have 7.9, 7.10.2, 7.22 — lexicographic MIN would give "7.10.2", not "7.9"
|
|
1449
|
+
expect(stats.ros_version_min).toBe("7.9");
|
|
1450
|
+
expect(stats.ros_version_max).toBe("7.22");
|
|
1451
|
+
});
|
|
1039
1452
|
});
|