@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/README.md +13 -1
- package/package.json +1 -1
- package/src/db.ts +18 -2
- package/src/extract-test-results.ts +86 -13
- package/src/mcp-http.test.ts +168 -23
- package/src/mcp.ts +130 -2
- package/src/paths.ts +8 -0
- package/src/query.test.ts +321 -14
- package/src/query.ts +327 -29
- package/src/setup.ts +7 -2
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(
|
|
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(
|
|
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(
|
|
733
|
-
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);
|
|
734
891
|
});
|
|
735
892
|
|
|
736
893
|
test("filter by has_wireless", () => {
|
|
737
894
|
const res = searchDevices("", { has_wireless: true });
|
|
738
|
-
expect(res.results).toHaveLength(
|
|
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(
|
|
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
|
-
|
|
814
|
-
expect(
|
|
815
|
-
expect(
|
|
816
|
-
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);
|
|
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("
|
|
1166
|
+
test("result shape has expected fields and no cpu fields", () => {
|
|
903
1167
|
const res = searchDeviceTests({ test_type: "ethernet" });
|
|
904
|
-
|
|
905
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|