@tx5dr/contracts 1.7.3 → 1.7.5

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.
Files changed (109) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +6 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/schema/__tests__/ability.test.d.ts +2 -0
  6. package/dist/schema/__tests__/ability.test.d.ts.map +1 -0
  7. package/dist/schema/__tests__/ability.test.js +32 -0
  8. package/dist/schema/__tests__/ability.test.js.map +1 -0
  9. package/dist/schema/__tests__/cw-decoder.schema.test.d.ts +2 -0
  10. package/dist/schema/__tests__/cw-decoder.schema.test.d.ts.map +1 -0
  11. package/dist/schema/__tests__/cw-decoder.schema.test.js +114 -0
  12. package/dist/schema/__tests__/cw-decoder.schema.test.js.map +1 -0
  13. package/dist/schema/__tests__/device-ui.schema.test.d.ts +2 -0
  14. package/dist/schema/__tests__/device-ui.schema.test.d.ts.map +1 -0
  15. package/dist/schema/__tests__/device-ui.schema.test.js +111 -0
  16. package/dist/schema/__tests__/device-ui.schema.test.js.map +1 -0
  17. package/dist/schema/__tests__/process-monitor.schema.test.js +96 -0
  18. package/dist/schema/__tests__/process-monitor.schema.test.js.map +1 -1
  19. package/dist/schema/__tests__/radio.schema.test.d.ts +2 -0
  20. package/dist/schema/__tests__/radio.schema.test.d.ts.map +1 -0
  21. package/dist/schema/__tests__/radio.schema.test.js +143 -0
  22. package/dist/schema/__tests__/radio.schema.test.js.map +1 -0
  23. package/dist/schema/ability.d.ts +3 -1
  24. package/dist/schema/ability.d.ts.map +1 -1
  25. package/dist/schema/ability.js +11 -0
  26. package/dist/schema/ability.js.map +1 -1
  27. package/dist/schema/api-response.schema.d.ts +168 -1
  28. package/dist/schema/api-response.schema.d.ts.map +1 -1
  29. package/dist/schema/api-response.schema.js +22 -0
  30. package/dist/schema/api-response.schema.js.map +1 -1
  31. package/dist/schema/cpu-profile.schema.d.ts +6 -6
  32. package/dist/schema/cw-decoder.schema.d.ts +2501 -0
  33. package/dist/schema/cw-decoder.schema.d.ts.map +1 -0
  34. package/dist/schema/cw-decoder.schema.js +196 -0
  35. package/dist/schema/cw-decoder.schema.js.map +1 -0
  36. package/dist/schema/cw-keyer.schema.d.ts +368 -0
  37. package/dist/schema/cw-keyer.schema.d.ts.map +1 -0
  38. package/dist/schema/cw-keyer.schema.js +143 -0
  39. package/dist/schema/cw-keyer.schema.js.map +1 -0
  40. package/dist/schema/desktop-https.schema.js +1 -1
  41. package/dist/schema/desktop-https.schema.js.map +1 -1
  42. package/dist/schema/device-ui.schema.d.ts +1782 -0
  43. package/dist/schema/device-ui.schema.d.ts.map +1 -0
  44. package/dist/schema/device-ui.schema.js +156 -0
  45. package/dist/schema/device-ui.schema.js.map +1 -0
  46. package/dist/schema/mode.schema.d.ts +1 -0
  47. package/dist/schema/mode.schema.d.ts.map +1 -1
  48. package/dist/schema/mode.schema.js +8 -0
  49. package/dist/schema/mode.schema.js.map +1 -1
  50. package/dist/schema/plugin.schema.d.ts +905 -60
  51. package/dist/schema/plugin.schema.d.ts.map +1 -1
  52. package/dist/schema/plugin.schema.js +62 -2
  53. package/dist/schema/plugin.schema.js.map +1 -1
  54. package/dist/schema/process-monitor.schema.d.ts +1948 -261
  55. package/dist/schema/process-monitor.schema.d.ts.map +1 -1
  56. package/dist/schema/process-monitor.schema.js +11 -0
  57. package/dist/schema/process-monitor.schema.js.map +1 -1
  58. package/dist/schema/radio-capability.schema.d.ts +1 -1
  59. package/dist/schema/radio-capability.schema.d.ts.map +1 -1
  60. package/dist/schema/radio-capability.schema.js +41 -0
  61. package/dist/schema/radio-capability.schema.js.map +1 -1
  62. package/dist/schema/radio-power.schema.d.ts +6 -0
  63. package/dist/schema/radio-power.schema.d.ts.map +1 -1
  64. package/dist/schema/radio-profile.schema.d.ts +86 -0
  65. package/dist/schema/radio-profile.schema.d.ts.map +1 -1
  66. package/dist/schema/radio.schema.d.ts +160 -3
  67. package/dist/schema/radio.schema.d.ts.map +1 -1
  68. package/dist/schema/radio.schema.js +82 -0
  69. package/dist/schema/radio.schema.js.map +1 -1
  70. package/dist/schema/realtime.schema.d.ts +6 -6
  71. package/dist/schema/voice.schema.d.ts +1 -1
  72. package/dist/schema/voice.schema.d.ts.map +1 -1
  73. package/dist/schema/voice.schema.js +1 -1
  74. package/dist/schema/voice.schema.js.map +1 -1
  75. package/dist/schema/websocket.schema.d.ts +6071 -574
  76. package/dist/schema/websocket.schema.d.ts.map +1 -1
  77. package/dist/schema/websocket.schema.js +102 -2
  78. package/dist/schema/websocket.schema.js.map +1 -1
  79. package/dist/utils/__tests__/cwTiming.test.d.ts +2 -0
  80. package/dist/utils/__tests__/cwTiming.test.d.ts.map +1 -0
  81. package/dist/utils/__tests__/cwTiming.test.js +74 -0
  82. package/dist/utils/__tests__/cwTiming.test.js.map +1 -0
  83. package/dist/utils/cwTiming.d.ts +36 -0
  84. package/dist/utils/cwTiming.d.ts.map +1 -0
  85. package/dist/utils/cwTiming.js +155 -0
  86. package/dist/utils/cwTiming.js.map +1 -0
  87. package/package.json +1 -1
  88. package/src/index.ts +8 -0
  89. package/src/schema/__tests__/ability.test.ts +40 -0
  90. package/src/schema/__tests__/cw-decoder.schema.test.ts +127 -0
  91. package/src/schema/__tests__/device-ui.schema.test.ts +122 -0
  92. package/src/schema/__tests__/process-monitor.schema.test.ts +99 -0
  93. package/src/schema/__tests__/radio.schema.test.ts +160 -0
  94. package/src/schema/ability.ts +13 -0
  95. package/src/schema/api-response.schema.ts +22 -0
  96. package/src/schema/cw-decoder.schema.ts +234 -0
  97. package/src/schema/cw-keyer.schema.ts +177 -0
  98. package/src/schema/desktop-https.schema.ts +1 -1
  99. package/src/schema/device-ui.schema.ts +182 -0
  100. package/src/schema/mode.schema.ts +8 -0
  101. package/src/schema/plugin.schema.ts +80 -2
  102. package/src/schema/process-monitor.schema.ts +18 -0
  103. package/src/schema/radio-capability.schema.ts +41 -0
  104. package/src/schema/radio.schema.ts +97 -0
  105. package/src/schema/voice.schema.ts +1 -1
  106. package/src/schema/websocket.schema.ts +131 -2
  107. package/src/utils/__tests__/cwTiming.test.ts +91 -0
  108. package/src/utils/cwTiming.ts +203 -0
  109. package/test/plugin-slot-schema.test.ts +52 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cwTiming.test.js","sourceRoot":"","sources":["../../../src/utils/__tests__/cwTiming.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * CW text to Morse timing encoder.
3
+ *
4
+ * Timing follows the ITU/PARIS convention:
5
+ * - dit = 1200 / WPM ms
6
+ * - dah = 3 dits
7
+ * - intra-character gap = 1 dit
8
+ * - character gap = 3 dits
9
+ * - word gap = 7 dits
10
+ */
11
+ export interface CWTimingEvent {
12
+ type: 'key-down' | 'key-up';
13
+ /** Delay from the previous event, in milliseconds. */
14
+ afterMs: number;
15
+ }
16
+ export interface CWKeyStateSegment {
17
+ /** true when the key/tone is closed, false for silence/open key. */
18
+ keyDown: boolean;
19
+ /** Segment duration in milliseconds. */
20
+ durationMs: number;
21
+ }
22
+ /**
23
+ * Encodes supported CW text into a sequence of timing events.
24
+ */
25
+ export declare function encodeTextToCWEvents(text: string, wpm: number): CWTimingEvent[];
26
+ /**
27
+ * Encodes supported CW text into key-down/silence duration segments.
28
+ * This is the shared adapter input for browser sidetone and other local keying outputs.
29
+ */
30
+ export declare function encodeTextToCWKeyStateSegments(text: string, wpm: number): CWKeyStateSegment[];
31
+ /**
32
+ * Estimates total CW message duration from the first key-down to the final key-up.
33
+ * Uses the PARIS timing base from Hamlib: dot milliseconds = 1200 / WPM.
34
+ */
35
+ export declare function estimateCWMessageDurationMs(text: string, wpm: number): number;
36
+ //# sourceMappingURL=cwTiming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cwTiming.d.ts","sourceRoot":"","sources":["../../src/utils/cwTiming.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC5B,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;CACpB;AAuCD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,EAAE,CAgB/E;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAkB7F;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAO7E"}
@@ -0,0 +1,155 @@
1
+ const DOT_UNITS = {
2
+ dit: 1,
3
+ dah: 3,
4
+ intraChar: 1,
5
+ interChar: 3,
6
+ interWord: 7,
7
+ };
8
+ const MORSE_TABLE = {
9
+ A: '.-', B: '-...', C: '-.-.', D: '-..',
10
+ E: '.', F: '..-.', G: '--.', H: '....',
11
+ I: '..', J: '.---', K: '-.-', L: '.-..',
12
+ M: '--', N: '-.', O: '---', P: '.--.',
13
+ Q: '--.-', R: '.-.', S: '...', T: '-',
14
+ U: '..-', V: '...-', W: '.--', X: '-..-',
15
+ Y: '-.--', Z: '--..',
16
+ '0': '-----', '1': '.----', '2': '..---', '3': '...--',
17
+ '4': '....-', '5': '.....', '6': '-....', '7': '--...',
18
+ '8': '---..', '9': '----.',
19
+ '.': '.-.-.-', ',': '--..--', '?': '..--..', '/': '-..-.',
20
+ '@': '.--.-.', '=': '-...-', '-': '-....-', '+': '.-.-.',
21
+ ':': '---...', ';': '-.-.-.', '\'': '.----.', '"': '.-..-.',
22
+ '!': '-.-.--', '&': '.-...', '(': '-.--.', ')': '-.--.-',
23
+ '_': '..--.-', '$': '...-..-',
24
+ };
25
+ const PROSIGN_MAP = {
26
+ '<AR>': '.-.-.',
27
+ '<SK>': '...-.-',
28
+ '<BT>': '-...-',
29
+ '<KN>': '-.--.',
30
+ '<BK>': '-...-.-',
31
+ '<CL>': '-.-..-..',
32
+ };
33
+ const PROSIGN_TOKENS = Object.keys(PROSIGN_MAP).sort((a, b) => b.length - a.length);
34
+ /**
35
+ * Encodes supported CW text into a sequence of timing events.
36
+ */
37
+ export function encodeTextToCWEvents(text, wpm) {
38
+ const events = [];
39
+ let nextEventDelayMs = 0;
40
+ for (const segment of encodeTextToCWKeyStateSegments(text, wpm)) {
41
+ if (!segment.keyDown) {
42
+ nextEventDelayMs += segment.durationMs;
43
+ continue;
44
+ }
45
+ events.push({ type: 'key-down', afterMs: nextEventDelayMs });
46
+ events.push({ type: 'key-up', afterMs: segment.durationMs });
47
+ nextEventDelayMs = 0;
48
+ }
49
+ return events;
50
+ }
51
+ /**
52
+ * Encodes supported CW text into key-down/silence duration segments.
53
+ * This is the shared adapter input for browser sidetone and other local keying outputs.
54
+ */
55
+ export function encodeTextToCWKeyStateSegments(text, wpm) {
56
+ const dotMs = getRoundedDotMs(wpm);
57
+ const tokenizedWords = tokenizeText(text);
58
+ const segments = [];
59
+ let firstToken = true;
60
+ for (const word of tokenizedWords) {
61
+ for (let ti = 0; ti < word.length; ti += 1) {
62
+ if (!firstToken) {
63
+ appendSegment(segments, false, (ti === 0 ? DOT_UNITS.interWord : DOT_UNITS.interChar) * dotMs);
64
+ }
65
+ appendCodeSegments(segments, word[ti], dotMs);
66
+ firstToken = false;
67
+ }
68
+ }
69
+ return segments;
70
+ }
71
+ /**
72
+ * Estimates total CW message duration from the first key-down to the final key-up.
73
+ * Uses the PARIS timing base from Hamlib: dot milliseconds = 1200 / WPM.
74
+ */
75
+ export function estimateCWMessageDurationMs(text, wpm) {
76
+ const safeWpm = getSafeWpm(wpm);
77
+ const dotUnits = countCWMessageDotUnits(text);
78
+ if (dotUnits <= 0) {
79
+ return 0;
80
+ }
81
+ return Math.ceil(dotUnits * (1200 / safeWpm));
82
+ }
83
+ function appendCodeSegments(segments, code, dotMs) {
84
+ for (let si = 0; si < code.length; si += 1) {
85
+ if (si > 0) {
86
+ appendSegment(segments, false, DOT_UNITS.intraChar * dotMs);
87
+ }
88
+ appendSegment(segments, true, (code[si] === '-' ? DOT_UNITS.dah : DOT_UNITS.dit) * dotMs);
89
+ }
90
+ }
91
+ function appendSegment(segments, keyDown, durationMs) {
92
+ if (durationMs <= 0) {
93
+ return;
94
+ }
95
+ const previous = segments[segments.length - 1];
96
+ if (previous?.keyDown === keyDown) {
97
+ previous.durationMs += durationMs;
98
+ return;
99
+ }
100
+ segments.push({ keyDown, durationMs });
101
+ }
102
+ function countCWMessageDotUnits(text) {
103
+ const tokenizedWords = tokenizeText(text);
104
+ let firstToken = true;
105
+ let totalUnits = 0;
106
+ for (const word of tokenizedWords) {
107
+ for (let ti = 0; ti < word.length; ti += 1) {
108
+ if (!firstToken) {
109
+ totalUnits += ti === 0 ? DOT_UNITS.interWord : DOT_UNITS.interChar;
110
+ }
111
+ const code = word[ti];
112
+ for (let si = 0; si < code.length; si += 1) {
113
+ totalUnits += si === 0 ? 0 : DOT_UNITS.intraChar;
114
+ totalUnits += code[si] === '-' ? DOT_UNITS.dah : DOT_UNITS.dit;
115
+ }
116
+ firstToken = false;
117
+ }
118
+ }
119
+ return totalUnits;
120
+ }
121
+ function tokenizeText(text) {
122
+ const normalized = text.toUpperCase().trim();
123
+ if (!normalized) {
124
+ return [];
125
+ }
126
+ return normalized
127
+ .split(/\s+/)
128
+ .map(tokenizeWord)
129
+ .filter((word) => word.length > 0);
130
+ }
131
+ function tokenizeWord(word) {
132
+ const codes = [];
133
+ let index = 0;
134
+ while (index < word.length) {
135
+ const prosign = PROSIGN_TOKENS.find((token) => word.startsWith(token, index));
136
+ if (prosign) {
137
+ codes.push(PROSIGN_MAP[prosign]);
138
+ index += prosign.length;
139
+ continue;
140
+ }
141
+ const code = MORSE_TABLE[word[index]];
142
+ if (code) {
143
+ codes.push(code);
144
+ }
145
+ index += 1;
146
+ }
147
+ return codes;
148
+ }
149
+ function getRoundedDotMs(wpm) {
150
+ return Math.round(1200 / getSafeWpm(wpm));
151
+ }
152
+ function getSafeWpm(wpm) {
153
+ return Math.max(5, Math.min(60, Math.round(Number(wpm) || 20)));
154
+ }
155
+ //# sourceMappingURL=cwTiming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cwTiming.js","sourceRoot":"","sources":["../../src/utils/cwTiming.ts"],"names":[],"mappings":"AAuBA,MAAM,SAAS,GAAG;IAChB,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,CAAC;IACN,SAAS,EAAE,CAAC;IACZ,SAAS,EAAE,CAAC;IACZ,SAAS,EAAE,CAAC;CACJ,CAAC;AAEX,MAAM,WAAW,GAA2B;IAC1C,CAAC,EAAE,IAAI,EAAK,CAAC,EAAE,MAAM,EAAG,CAAC,EAAE,MAAM,EAAG,CAAC,EAAE,KAAK;IAC5C,CAAC,EAAE,GAAG,EAAM,CAAC,EAAE,MAAM,EAAG,CAAC,EAAE,KAAK,EAAI,CAAC,EAAE,MAAM;IAC7C,CAAC,EAAE,IAAI,EAAK,CAAC,EAAE,MAAM,EAAG,CAAC,EAAE,KAAK,EAAI,CAAC,EAAE,MAAM;IAC7C,CAAC,EAAE,IAAI,EAAK,CAAC,EAAE,IAAI,EAAK,CAAC,EAAE,KAAK,EAAI,CAAC,EAAE,MAAM;IAC7C,CAAC,EAAE,MAAM,EAAG,CAAC,EAAE,KAAK,EAAI,CAAC,EAAE,KAAK,EAAI,CAAC,EAAE,GAAG;IAC1C,CAAC,EAAE,KAAK,EAAI,CAAC,EAAE,MAAM,EAAG,CAAC,EAAE,KAAK,EAAI,CAAC,EAAE,MAAM;IAC7C,CAAC,EAAE,MAAM,EAAG,CAAC,EAAE,MAAM;IACrB,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO;IACtD,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO;IACtD,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO;IAC1B,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO;IACzD,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO;IACxD,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ;IAC3D,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ;IACxD,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS;CAC9B,CAAC;AAEF,MAAM,WAAW,GAA2B;IAC1C,MAAM,EAAE,OAAO;IACf,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,OAAO;IACf,MAAM,EAAE,OAAO;IACf,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,UAAU;CACnB,CAAC;AAEF,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpF;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,GAAW;IAC5D,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,IAAI,gBAAgB,GAAG,CAAC,CAAC;IAEzB,KAAK,MAAM,OAAO,IAAI,8BAA8B,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QAChE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YACrB,gBAAgB,IAAI,OAAO,CAAC,UAAU,CAAC;YACvC,SAAS;QACX,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAC;QAC7D,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;QAC7D,gBAAgB,GAAG,CAAC,CAAC;IACvB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,8BAA8B,CAAC,IAAY,EAAE,GAAW;IACtE,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IACnC,MAAM,cAAc,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAwB,EAAE,CAAC;IACzC,IAAI,UAAU,GAAG,IAAI,CAAC;IAEtB,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;QAClC,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;YAC3C,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,aAAa,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC,CAAC;YACjG,CAAC;YAED,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;YAC9C,UAAU,GAAG,KAAK,CAAC;QACrB,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,2BAA2B,CAAC,IAAY,EAAE,GAAW;IACnE,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,QAAQ,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;QAClB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC;AAChD,CAAC;AAED,SAAS,kBAAkB,CAAC,QAA6B,EAAE,IAAY,EAAE,KAAa;IACpF,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;QAC3C,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;YACX,aAAa,CAAC,QAAQ,EAAE,KAAK,EAAE,SAAS,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC;QAC9D,CAAC;QACD,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAC5F,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,QAA6B,EAAE,OAAgB,EAAE,UAAkB;IACxF,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;QACpB,OAAO;IACT,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/C,IAAI,QAAQ,EAAE,OAAO,KAAK,OAAO,EAAE,CAAC;QAClC,QAAQ,CAAC,UAAU,IAAI,UAAU,CAAC;QAClC,OAAO;IACT,CAAC;IACD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,sBAAsB,CAAC,IAAY;IAC1C,MAAM,cAAc,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,UAAU,GAAG,IAAI,CAAC;IACtB,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;QAClC,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;YAC3C,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,UAAU,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC;YACrE,CAAC;YAED,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;YACtB,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;gBAC3C,UAAU,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC;gBACjD,UAAU,IAAI,IAAI,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC;YACjE,CAAC;YACD,UAAU,GAAG,KAAK,CAAC;QACrB,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;IAC7C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,UAAU;SACd,KAAK,CAAC,KAAK,CAAC;SACZ,GAAG,CAAC,YAAY,CAAC;SACjB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,OAAO,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QAC9E,IAAI,OAAO,EAAE,CAAC;YACZ,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;YACjC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;YACxB,SAAS;QACX,CAAC;QAED,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QACtC,IAAI,IAAI,EAAE,CAAC;YACT,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QACD,KAAK,IAAI,CAAC,CAAC;IACb,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,eAAe,CAAC,GAAW;IAClC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tx5dr/contracts",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -118,12 +118,17 @@ export * from './schema/system.schema.js';
118
118
  export * from './schema/voice.schema.js';
119
119
  export * from './schema/voice-keyer.schema.js';
120
120
 
121
+ // CW Keyer Schema
122
+ export * from './schema/cw-keyer.schema.js';
123
+ export * from './schema/cw-decoder.schema.js';
124
+
121
125
  // Station Info Schema
122
126
  export * from './schema/station-info.schema.js';
123
127
 
124
128
  // Grid Utils
125
129
  export * from './utils/grid.js';
126
130
  export * from './utils/callsign.js';
131
+ export * from './utils/cwTiming.js';
127
132
 
128
133
  // Process Monitor Schema
129
134
  export * from './schema/process-monitor.schema.js';
@@ -153,6 +158,9 @@ export * from './schema/realtime.schema.js';
153
158
  // Desktop HTTPS Schema
154
159
  export * from './schema/desktop-https.schema.js';
155
160
 
161
+ // Device UI Schema
162
+ export * from './schema/device-ui.schema.js';
163
+
156
164
  // Plugin Schema
157
165
  export * from './schema/plugin.schema.js';
158
166
  export * from './schema/update.schema.js';
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ buildAbilityRules,
4
+ Permission,
5
+ PERMISSION_GROUPS,
6
+ PERMISSION_RULE_MAP,
7
+ } from '../ability.js';
8
+ import { UserRole } from '../auth.schema.js';
9
+
10
+ describe('ability contracts', () => {
11
+ it('maps CW decoder permissions to separate control and config capabilities', () => {
12
+ expect(PERMISSION_RULE_MAP[Permission.CW_DECODER_CONTROL]).toEqual({
13
+ action: 'execute',
14
+ subject: 'CWDecoder',
15
+ });
16
+ expect(PERMISSION_RULE_MAP[Permission.CW_DECODER_CONFIG]).toEqual({
17
+ action: 'update',
18
+ subject: 'CWDecoderConfig',
19
+ });
20
+ });
21
+
22
+ it('keeps CW decoder permissions delegatable instead of granting them to every operator', () => {
23
+ expect(buildAbilityRules({ role: UserRole.OPERATOR, operatorIds: ['op1'] }))
24
+ .not.toContainEqual({ action: 'execute', subject: 'CWDecoder' });
25
+
26
+ expect(buildAbilityRules({
27
+ role: UserRole.OPERATOR,
28
+ operatorIds: ['op1'],
29
+ permissionGrants: [{ permission: Permission.CW_DECODER_CONTROL }],
30
+ })).toContainEqual({ action: 'execute', subject: 'CWDecoder' });
31
+ });
32
+
33
+ it('exposes CW permissions in the token permission groups', () => {
34
+ const cwGroup = PERMISSION_GROUPS.find(group => group.key === 'cw');
35
+ expect(cwGroup?.permissions).toEqual([
36
+ Permission.CW_DECODER_CONTROL,
37
+ Permission.CW_DECODER_CONFIG,
38
+ ]);
39
+ });
40
+ });
@@ -0,0 +1,127 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ CWDecoderConfigSchema,
4
+ CWDecoderEventSchema,
5
+ CWDecoderStatusSchema,
6
+ CWDecoderTuningUpdateSchema,
7
+ } from '../cw-decoder.schema.js';
8
+
9
+ describe('CW decoder contracts', () => {
10
+ it('parses v1 defaults', () => {
11
+ expect(CWDecoderConfigSchema.parse({})).toMatchObject({
12
+ enabled: false,
13
+ backend: 'deepcw-onnx',
14
+ runtimeBackend: 'cpu',
15
+ modelSize: 'tiny',
16
+ language: 'en',
17
+ mode: 'streaming',
18
+ targetFreqHz: 800,
19
+ filterWidthHz: 800,
20
+ windowSeconds: 12,
21
+ decodeIntervalMs: 1000,
22
+ muteWhileTransmitting: true,
23
+ workerCount: 1,
24
+ minCommitChars: 1,
25
+ commitStability: 2,
26
+ maxPendingAgeMs: 4000,
27
+ });
28
+ });
29
+
30
+ it('accepts pending and commit transcript events', () => {
31
+ expect(CWDecoderEventSchema.parse({
32
+ kind: 'pending',
33
+ text: 'CQ TEST',
34
+ confidence: 0.8,
35
+ timestamp: 1,
36
+ })).toMatchObject({ kind: 'pending', text: 'CQ TEST' });
37
+
38
+ expect(CWDecoderEventSchema.parse({
39
+ kind: 'commit',
40
+ text: 'CQ TEST',
41
+ confidence: 0.9,
42
+ timestamp: 2,
43
+ segment: {
44
+ id: 'seg-1',
45
+ sessionId: 'session-1',
46
+ sequence: 1,
47
+ text: 'CQ TEST',
48
+ startedAt: 1,
49
+ updatedAt: 2,
50
+ endedAt: 2,
51
+ confidence: 0.9,
52
+ finalized: true,
53
+ prependSpace: true,
54
+ },
55
+ })).toMatchObject({ kind: 'commit', segment: { finalized: true } });
56
+ });
57
+
58
+ it('accepts structured transcript reset, pending, and commit events', () => {
59
+ expect(CWDecoderEventSchema.parse({
60
+ kind: 'transcript_reset',
61
+ sessionId: 'session-1',
62
+ timestamp: 1,
63
+ })).toMatchObject({ kind: 'transcript_reset', sessionId: 'session-1' });
64
+
65
+ expect(CWDecoderEventSchema.parse({
66
+ kind: 'transcript_pending',
67
+ pending: {
68
+ sessionId: 'session-1',
69
+ version: 1,
70
+ text: 'CQ TE',
71
+ plainText: 'CQ TE',
72
+ finalized: false,
73
+ confidence: 0.7,
74
+ targetFreqHz: 800,
75
+ filterWidthHz: 800,
76
+ updatedAt: 2,
77
+ },
78
+ timestamp: 2,
79
+ })).toMatchObject({ kind: 'transcript_pending', pending: { finalized: false, version: 1 } });
80
+
81
+ expect(CWDecoderEventSchema.parse({
82
+ kind: 'transcript_commit',
83
+ segment: {
84
+ id: 'seg-1',
85
+ sessionId: 'session-1',
86
+ sequence: 1,
87
+ text: 'CQ TEST',
88
+ plainText: 'CQ TEST',
89
+ finalized: true,
90
+ prependSpace: false,
91
+ confidence: 0.9,
92
+ targetFreqHz: 800,
93
+ filterWidthHz: 800,
94
+ characterSpans: [{ char: 'C', startFrame: 1, endFrame: 2 }],
95
+ wordSpaceSpans: [{ startFrame: 10, endFrame: 12 }],
96
+ startedAt: 1,
97
+ endedAt: 3,
98
+ updatedAt: 3,
99
+ },
100
+ timestamp: 3,
101
+ })).toMatchObject({ kind: 'transcript_commit', segment: { sequence: 1, prependSpace: false } });
102
+ });
103
+
104
+ it('rejects model sizes that are not packaged in v1', () => {
105
+ expect(() => CWDecoderConfigSchema.parse({ modelSize: 'base' })).toThrow();
106
+ });
107
+
108
+ it('validates runtime tuning updates', () => {
109
+ expect(CWDecoderTuningUpdateSchema.parse({ targetFreqHz: 650 })).toEqual({ targetFreqHz: 650 });
110
+ expect(CWDecoderTuningUpdateSchema.parse({ filterWidthHz: 250 })).toEqual({ filterWidthHz: 250 });
111
+ expect(() => CWDecoderTuningUpdateSchema.parse({})).toThrow();
112
+ expect(() => CWDecoderTuningUpdateSchema.parse({ targetFreqHz: 50 })).toThrow();
113
+ expect(() => CWDecoderTuningUpdateSchema.parse({ filterWidthHz: 900 })).toThrow();
114
+ });
115
+
116
+ it('represents muted listening state', () => {
117
+ const config = CWDecoderConfigSchema.parse({ enabled: true });
118
+ expect(CWDecoderStatusSchema.parse({
119
+ enabled: true,
120
+ state: 'muted',
121
+ config,
122
+ muted: true,
123
+ active: false,
124
+ updatedAt: 3,
125
+ })).toMatchObject({ state: 'muted', muted: true });
126
+ });
127
+ });
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ DeviceUiBootstrapSnapshotSchema,
5
+ DeviceUiWsEventSchema,
6
+ DeviceUiJwtPayloadSchema,
7
+ DeviceUiSessionRequestSchema,
8
+ DeviceUiSessionResponseSchema,
9
+ } from '../device-ui.schema.js';
10
+
11
+ describe('device UI schemas', () => {
12
+ it('accepts the dedicated device UI JWT payload only', () => {
13
+ expect(DeviceUiJwtPayloadSchema.parse({
14
+ typ: 'device-ui',
15
+ aud: 'tx5dr-device-ui',
16
+ deviceId: 'panel-1',
17
+ sessionId: 'session-1',
18
+ iat: 1,
19
+ exp: 2,
20
+ }).aud).toBe('tx5dr-device-ui');
21
+
22
+ expect(() => DeviceUiJwtPayloadSchema.parse({
23
+ typ: 'access',
24
+ aud: 'tx5dr-device-ui',
25
+ deviceId: 'panel-1',
26
+ sessionId: 'session-1',
27
+ iat: 1,
28
+ exp: 2,
29
+ })).toThrow();
30
+ });
31
+
32
+ it('describes session request and response contracts', () => {
33
+ expect(DeviceUiSessionRequestSchema.parse({ deviceId: 'panel-1', sessionToken: 'secret' }).deviceId).toBe('panel-1');
34
+ expect(DeviceUiSessionResponseSchema.parse({
35
+ jwt: 'jwt',
36
+ deviceId: 'panel-1',
37
+ sessionId: 'session-1',
38
+ expiresAt: 1_700_000_001_000,
39
+ }).sessionId).toBe('session-1');
40
+ });
41
+
42
+ it('describes the mode-aware bootstrap snapshot and WS event', () => {
43
+ const snapshot = DeviceUiBootstrapSnapshotSchema.parse({
44
+ server: { status: 'ok', version: 'test', webPort: 8076 },
45
+ station: { callsign: 'BG5DRB', callsigns: ['BG5DRB', 'BG5AAA'] },
46
+ operators: [{ id: 'op1', callsign: 'BG5AAA', active: true, transmitting: false, ptt: false }],
47
+ engine: { running: true, mode: 'digital', currentMode: { name: 'FT8', slotMs: 15000 }, state: 'running' },
48
+ radio: { connected: true, frequency: 7074000, radioMode: 'USB-D', ptt: false, tx: false },
49
+ ft8: {
50
+ slot: null,
51
+ utc: null,
52
+ cycle: null,
53
+ periodMs: 15000,
54
+ recentDecodeRawMessages: ['CQ TEST AA00'],
55
+ lastDecodeRawMessage: 'CQ TEST AA00',
56
+ recentFramesSlotId: 'FT8-1',
57
+ recentFramesSlotStartMs: 1_700_000_000_000,
58
+ recentFrames: [{
59
+ slotId: 'FT8-1',
60
+ slotStartMs: 1_700_000_000_000,
61
+ snr: -10,
62
+ freq: 1200,
63
+ dt: 0.1,
64
+ message: 'CQ TEST AA00',
65
+ operatorId: null,
66
+ countryZh: '测试地区',
67
+ countryEn: 'Test Region',
68
+ }],
69
+ currentTx: { active: false, operatorIds: [], messages: [], lastMessage: null, slotStartMs: null },
70
+ },
71
+ voice: {
72
+ active: false,
73
+ radioMode: null,
74
+ pttLocked: false,
75
+ pttLockedByLabel: null,
76
+ keyerActive: false,
77
+ keyerMode: null,
78
+ keyerSlotId: null,
79
+ },
80
+ cw: {
81
+ decoder: {
82
+ enabled: false,
83
+ active: false,
84
+ state: 'disabled',
85
+ muted: false,
86
+ pendingText: '',
87
+ committedText: '',
88
+ lastDecodeAt: null,
89
+ updatedAt: 1,
90
+ },
91
+ keyer: {
92
+ active: false,
93
+ mode: null,
94
+ messageId: null,
95
+ currentText: null,
96
+ lastText: null,
97
+ },
98
+ currentTx: {
99
+ active: false,
100
+ messages: [],
101
+ lastMessage: null,
102
+ },
103
+ },
104
+ access: { localUrl: 'http://192.168.1.10:8076', localUrls: ['http://192.168.1.10:8076'] },
105
+ updatedAt: 1,
106
+ });
107
+
108
+ expect(snapshot.station.callsign).toBe('BG5DRB');
109
+ expect(snapshot.station.callsigns).toEqual(['BG5DRB', 'BG5AAA']);
110
+ expect(snapshot.operators[0]?.callsign).toBe('BG5AAA');
111
+ expect(snapshot.ft8.recentFramesSlotId).toBe('FT8-1');
112
+ expect(snapshot.ft8.recentFrames[0]?.slotId).toBe('FT8-1');
113
+ expect(snapshot.ft8.recentFrames[0]?.countryZh).toBe('测试地区');
114
+ expect(snapshot.ft8.lastDecodeRawMessage).toBe('CQ TEST AA00');
115
+ expect(snapshot.cw.decoder.state).toBe('disabled');
116
+ expect(DeviceUiWsEventSchema.parse({
117
+ type: 'snapshot',
118
+ payload: snapshot,
119
+ timestamp: '2026-05-14T00:00:00.000Z',
120
+ }).payload.server.status).toBe('ok');
121
+ });
122
+ });
@@ -143,4 +143,103 @@ describe('ProcessSnapshotSchema', () => {
143
143
  expect(parsed.decodeWorkers?.summary.status).toBe('stopped');
144
144
  expect(parsed.decodeWorkers?.summary.lastError).toBeUndefined();
145
145
  });
146
+
147
+ it('accepts generic worker pool telemetry while preserving decodeWorkers compatibility', () => {
148
+ const decodeWorkerTelemetry = {
149
+ summary: {
150
+ status: 'ready' as const,
151
+ workerCount: 1,
152
+ desiredWorkers: 1,
153
+ readyCount: 1,
154
+ busyCount: 0,
155
+ totalRss: 256,
156
+ totalCpu: 12,
157
+ nativeThreadsPerWorker: 2,
158
+ pendingJobs: 0,
159
+ activeJobs: 0,
160
+ },
161
+ workers: [
162
+ {
163
+ workerId: 1,
164
+ pid: 1234,
165
+ ready: true,
166
+ busy: false,
167
+ nativeThreads: 2,
168
+ uptimeSeconds: 30,
169
+ memory: {
170
+ heapUsed: 100,
171
+ heapTotal: 200,
172
+ rss: 256,
173
+ external: 20,
174
+ arrayBuffers: 10,
175
+ },
176
+ cpu: {
177
+ user: 10,
178
+ system: 2,
179
+ total: 12,
180
+ },
181
+ lastSeenAt: 2,
182
+ },
183
+ ],
184
+ };
185
+
186
+ const parsed = ProcessSnapshotSchema.parse({
187
+ ...createBaseSnapshot(),
188
+ decodeWorkers: decodeWorkerTelemetry,
189
+ workerPools: [
190
+ {
191
+ id: 'ft8-decode',
192
+ name: 'FT8 Decode Workers',
193
+ kind: 'decode',
194
+ ...decodeWorkerTelemetry,
195
+ },
196
+ {
197
+ id: 'cw-decoder',
198
+ name: 'CW Decoder Workers',
199
+ kind: 'cw-decoder',
200
+ summary: {
201
+ status: 'ready',
202
+ workerCount: 1,
203
+ desiredWorkers: 1,
204
+ readyCount: 1,
205
+ busyCount: 0,
206
+ totalRss: 512,
207
+ totalCpu: 4,
208
+ nativeThreadsPerWorker: 1,
209
+ pendingJobs: 0,
210
+ activeJobs: 0,
211
+ },
212
+ workers: [
213
+ {
214
+ workerId: 1,
215
+ pid: 5678,
216
+ ready: true,
217
+ busy: false,
218
+ nativeThreads: 1,
219
+ uptimeSeconds: 8,
220
+ memory: {
221
+ heapUsed: 120,
222
+ heapTotal: 220,
223
+ rss: 512,
224
+ external: 30,
225
+ arrayBuffers: 15,
226
+ },
227
+ cpu: {
228
+ user: 3,
229
+ system: 1,
230
+ total: 4,
231
+ },
232
+ lastSeenAt: 3,
233
+ },
234
+ ],
235
+ },
236
+ ],
237
+ });
238
+
239
+ expect(parsed.decodeWorkers?.summary.workerCount).toBe(1);
240
+ expect(parsed.workerPools?.map((pool) => pool.id)).toEqual(['ft8-decode', 'cw-decoder']);
241
+ expect(parsed.workerPools?.[0].workers[0].workerId).toBe(1);
242
+ expect(parsed.workerPools?.[1].summary.totalRss).toBe(512);
243
+ expect(parsed.workerPools?.[1].workers[0].pid).toBe(5678);
244
+ });
146
245
  });