@tx5dr/plugin-api 1.7.11 → 2.0.0
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 +274 -17
- package/dist/__tests__/adif.test.js +40 -0
- package/dist/__tests__/adif.test.js.map +1 -1
- package/dist/__tests__/capability-context.test.d.ts +2 -0
- package/dist/__tests__/capability-context.test.d.ts.map +1 -0
- package/dist/__tests__/capability-context.test.js +99 -0
- package/dist/__tests__/capability-context.test.js.map +1 -0
- package/dist/__tests__/testing-utils.test.js +235 -14
- package/dist/__tests__/testing-utils.test.js.map +1 -1
- package/dist/capabilities.d.ts +37 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +48 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/context.d.ts +95 -56
- package/dist/context.d.ts.map +1 -1
- package/dist/definition.d.ts +73 -20
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js +8 -1
- package/dist/definition.js.map +1 -1
- package/dist/helpers.d.ts +340 -129
- package/dist/helpers.d.ts.map +1 -1
- package/dist/hooks.d.ts +54 -34
- package/dist/hooks.d.ts.map +1 -1
- package/dist/host-dependencies.d.ts +101 -0
- package/dist/host-dependencies.d.ts.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +164 -9
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +9 -1
- package/dist/runtime.js.map +1 -1
- package/dist/settings.d.ts +31 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/sync.d.ts +98 -6
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +6 -0
- package/dist/sync.js.map +1 -1
- package/dist/testing/index.d.ts +70 -12
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +307 -104
- package/dist/testing/index.js.map +1 -1
- package/dist/utils/adif.d.ts.map +1 -1
- package/dist/utils/adif.js +6 -2
- package/dist/utils/adif.js.map +1 -1
- package/dist/utils/qso-text-fields.d.ts +7 -1
- package/dist/utils/qso-text-fields.d.ts.map +1 -1
- package/dist/utils/qso-text-fields.js +74 -6
- package/dist/utils/qso-text-fields.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -15,12 +15,14 @@ npm install --save-dev @tx5dr/plugin-api
|
|
|
15
15
|
### TypeScript
|
|
16
16
|
|
|
17
17
|
```typescript
|
|
18
|
-
import
|
|
18
|
+
import { definePlugin } from '@tx5dr/plugin-api';
|
|
19
19
|
|
|
20
|
-
const plugin
|
|
20
|
+
const plugin = definePlugin({
|
|
21
|
+
apiVersion: 2,
|
|
21
22
|
name: 'my-plugin',
|
|
22
23
|
version: '1.0.0',
|
|
23
24
|
type: 'utility',
|
|
25
|
+
permissions: [],
|
|
24
26
|
hooks: {
|
|
25
27
|
onDecode(messages, ctx) {
|
|
26
28
|
for (const msg of messages) {
|
|
@@ -28,7 +30,7 @@ const plugin: PluginDefinition = {
|
|
|
28
30
|
}
|
|
29
31
|
},
|
|
30
32
|
},
|
|
31
|
-
};
|
|
33
|
+
});
|
|
32
34
|
|
|
33
35
|
export default plugin;
|
|
34
36
|
```
|
|
@@ -36,11 +38,14 @@ export default plugin;
|
|
|
36
38
|
### JavaScript (with JSDoc types)
|
|
37
39
|
|
|
38
40
|
```javascript
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
import { definePlugin } from '@tx5dr/plugin-api';
|
|
42
|
+
|
|
43
|
+
export default definePlugin({
|
|
44
|
+
apiVersion: 2,
|
|
41
45
|
name: 'my-plugin',
|
|
42
46
|
version: '1.0.0',
|
|
43
47
|
type: 'utility',
|
|
48
|
+
permissions: [],
|
|
44
49
|
hooks: {
|
|
45
50
|
onDecode(messages, ctx) {
|
|
46
51
|
for (const msg of messages) {
|
|
@@ -48,32 +53,118 @@ export default {
|
|
|
48
53
|
}
|
|
49
54
|
},
|
|
50
55
|
},
|
|
51
|
-
};
|
|
56
|
+
});
|
|
52
57
|
```
|
|
53
58
|
|
|
54
59
|
## Exports
|
|
55
60
|
|
|
56
61
|
| Subpath | Description |
|
|
57
62
|
|---------|-------------|
|
|
58
|
-
| `@tx5dr/plugin-api` |
|
|
59
|
-
| `@tx5dr/plugin-api/testing` | Mock factories for unit testing: `createMockContext()`, `createMockSlotInfo()`, `createMockParsedMessage()` |
|
|
63
|
+
| `@tx5dr/plugin-api` | `definePlugin()`, capability-derived contexts, hooks, structured command ports, and radio/message types |
|
|
64
|
+
| `@tx5dr/plugin-api/testing` | Mock factories for unit testing: `createMockContext()`, `createMockSlotInfo()`, `createMockParsedMessage()`, `createMockEventBus()` |
|
|
60
65
|
| `@tx5dr/plugin-api/bridge` | Ambient type declarations for the iframe Bridge SDK (`window.tx5dr`) |
|
|
61
66
|
|
|
67
|
+
## Capability Model
|
|
68
|
+
|
|
69
|
+
Privileged Host APIs use an allowlist model. A plugin must declare every
|
|
70
|
+
capability in `permissions`; the Host then projects only those properties into
|
|
71
|
+
that plugin's context. Undeclared properties are absent from both the inferred
|
|
72
|
+
TypeScript type and the runtime object. Use `definePlugin()` without manually
|
|
73
|
+
widening callbacks to `PluginContext`, otherwise TypeScript cannot preserve the
|
|
74
|
+
literal permission tuple.
|
|
75
|
+
|
|
76
|
+
Capabilities grant structured Host ports, not physical device ownership. The
|
|
77
|
+
capability-derived context does not directly expose raw PTT, audio playback,
|
|
78
|
+
the mixer, encoder, physical frame lease, arbitrary radio capability writes,
|
|
79
|
+
or global emergency stop. Strategy runtimes receive a narrower speculative
|
|
80
|
+
context and return declarative decisions; they never receive command ports.
|
|
81
|
+
|
|
82
|
+
For a strategy plugin, `type: 'strategy'` plus `apiVersion: 2` is the explicit
|
|
83
|
+
declaration that it may produce RF decisions when selected by an operator. It
|
|
84
|
+
does not need `operator:transmit-control`. That permission is reserved for
|
|
85
|
+
utility plugins that need the imperative, Host-coordinated
|
|
86
|
+
`ctx.operatorCommands` port.
|
|
87
|
+
|
|
88
|
+
API v2 is required for strategy plugins and any plugin requesting a mutation
|
|
89
|
+
capability. The Host validates and freezes the loaded definition so permissions
|
|
90
|
+
cannot be expanded after load.
|
|
91
|
+
|
|
92
|
+
This is a Host API contract, not a sandbox for hostile Node.js code. Third-party
|
|
93
|
+
plugins currently execute in the server process; process isolation is a
|
|
94
|
+
separate security boundary.
|
|
95
|
+
|
|
96
|
+
## Data Ownership and Callback Lifetime
|
|
97
|
+
|
|
98
|
+
Configuration, hook arguments, query results and messages cross the Host boundary by value. Plugins may modify their local copies, but changes are not persisted until they call an explicit API such as `ctx.updateConfig()`, `store.set()` or a command port. UI/config/KV channels accept JSON-compatible values; hooks, strategy results and EventBus payloads accept structured-clone-compatible values. Functions, cycles, Host handles and other unsupported values are rejected with `PLUGIN_DATA_NOT_SERIALIZABLE`.
|
|
99
|
+
|
|
100
|
+
Host capabilities such as `ctx.ui`, `ctx.logbook`, radio command ports, network sockets and native `Response` objects are live handles. Use them only before the current Host callback settles. A handle retained past timeout, reload or unload rejects with `PLUGIN_INVOCATION_EXPIRED`; use Host timers and callbacks for later work instead of detached continuations.
|
|
101
|
+
|
|
102
|
+
## Operator Transmit Control
|
|
103
|
+
|
|
104
|
+
Utility plugins that need to submit operator commands must declare
|
|
105
|
+
`operator:transmit-control` and submit one of the high-level commands accepted
|
|
106
|
+
by `ctx.operatorCommands`. The permission grants API access; it does not by
|
|
107
|
+
itself classify the plugin as an automatic caller.
|
|
108
|
+
|
|
109
|
+
Automatic calling plugins implement `isAutoCallEnabled()`. This both gates the
|
|
110
|
+
command port and opts the plugin into the operator auto-call indicator and pause
|
|
111
|
+
controls:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const plugin = definePlugin({
|
|
115
|
+
apiVersion: 2,
|
|
116
|
+
name: 'scheduled-caller',
|
|
117
|
+
version: '1.0.0',
|
|
118
|
+
type: 'utility',
|
|
119
|
+
permissions: ['operator:transmit-control'],
|
|
120
|
+
isAutoCallEnabled: (ctx) => ctx.config.enabled === true,
|
|
121
|
+
hooks: {
|
|
122
|
+
async onTimer(_timerId, ctx) {
|
|
123
|
+
await ctx.operatorCommands.submit({
|
|
124
|
+
type: 'request-call',
|
|
125
|
+
callsign: 'W1AW',
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Integrations such as remote-control protocol bridges that may submit occasional
|
|
133
|
+
commands but do not autonomously originate calls implement
|
|
134
|
+
`isTransmitControlEnabled()` instead. They receive the same guarded command
|
|
135
|
+
port but are not shown or paused as auto-call plugins.
|
|
136
|
+
|
|
137
|
+
The Host allocates an operator command epoch and routes the request through the
|
|
138
|
+
operator/frame coordinators. Plugins cannot directly key or unkey the radio.
|
|
139
|
+
|
|
62
140
|
## Radio Permissions
|
|
63
141
|
|
|
64
|
-
|
|
142
|
+
`ctx.radio` always exposes a small read-only operating snapshot. Additional
|
|
143
|
+
radio capabilities require explicit declarations:
|
|
65
144
|
|
|
66
145
|
```ts
|
|
67
|
-
permissions: ['radio:read', 'radio:control', 'radio:power']
|
|
146
|
+
permissions: ['radio:read', 'radio:control', 'radio:tuner-control', 'radio:power']
|
|
68
147
|
```
|
|
69
148
|
|
|
70
|
-
- `radio:read`
|
|
71
|
-
- `radio:control`
|
|
72
|
-
- `
|
|
73
|
-
- `
|
|
149
|
+
- `radio:read` exposes `ctx.radioCapabilities` and the read-only `ctx.radioPower` view.
|
|
150
|
+
- `radio:control` exposes Host-arbitrated `set-frequency` and `switch-band` commands.
|
|
151
|
+
- `switch-band` can include `autoTune: true` when `radio:tuner-control` is also declared; the Host keeps the complete operation inside one physical-idle fence.
|
|
152
|
+
- `radio:tuner-control` exposes only `set-enabled` and `start-manual-tune` through `ctx.radioTunerCommands`.
|
|
153
|
+
- Radio writes reject while Digital, Voice, CW, Tune or manual PTT owns the physical transmitter; they never interrupt that transmission.
|
|
154
|
+
- `radio:power` exposes `ctx.radioPowerCommands.submit({ type: 'set-power', state })`.
|
|
155
|
+
- `ctx.radio.mode` remains read-only and uses ADIF `MODE`/`SUBMODE` semantics.
|
|
74
156
|
|
|
75
157
|
These APIs are not exposed directly to iframe pages; custom UI should call a server-side page handler.
|
|
76
158
|
|
|
159
|
+
## Logbook Permissions
|
|
160
|
+
|
|
161
|
+
- `logbook:read` exposes only query and worked-status methods on `ctx.logbook`.
|
|
162
|
+
- `logbook:write` exposes durable `addQSO()`/`updateQSO()` mutations in addition to reads.
|
|
163
|
+
- `logbook:sync` exposes `ctx.logbookSync` for registering a Host-managed sync provider.
|
|
164
|
+
|
|
165
|
+
Write completion means the Host logbook durability contract has completed.
|
|
166
|
+
Logbook APIs never grant PTT or frame lifecycle control.
|
|
167
|
+
|
|
77
168
|
## Host Settings Permissions
|
|
78
169
|
|
|
79
170
|
Server-side plugins can use `ctx.settings` to read or update a safe whitelist of host settings when the manifest declares the matching permission. Each settings namespace uses one read/write permission:
|
|
@@ -89,9 +180,10 @@ Server-side plugins can use `ctx.settings` to read or update a safe whitelist of
|
|
|
89
180
|
| `ctx.settings.ntp` | `settings:ntp` | `get()`, `update({ servers })` |
|
|
90
181
|
|
|
91
182
|
```ts
|
|
92
|
-
import
|
|
183
|
+
import { definePlugin } from '@tx5dr/plugin-api';
|
|
93
184
|
|
|
94
|
-
const plugin
|
|
185
|
+
const plugin = definePlugin({
|
|
186
|
+
apiVersion: 2,
|
|
95
187
|
name: 'station-policy',
|
|
96
188
|
version: '1.0.0',
|
|
97
189
|
type: 'utility',
|
|
@@ -102,13 +194,178 @@ const plugin: PluginDefinition = {
|
|
|
102
194
|
await ctx.settings.station.update({ callsign: 'W1AW' });
|
|
103
195
|
},
|
|
104
196
|
},
|
|
105
|
-
};
|
|
197
|
+
});
|
|
106
198
|
|
|
107
199
|
export default plugin;
|
|
108
200
|
```
|
|
109
201
|
|
|
110
202
|
The whitelist intentionally excludes authentication tokens, operator CRUD, hardware radio connection settings, audio devices, rigctld, OpenWebRX, profiles, and server host/port settings. These APIs are not exposed directly to iframe pages; custom UI should call a server-side page handler with `window.tx5dr.invoke()`.
|
|
111
203
|
|
|
204
|
+
## Plugin Event Bus
|
|
205
|
+
|
|
206
|
+
Server-side plugins can exchange in-process messages through `ctx.eventBus`, a topic-based pub/sub bus scoped to the host process. Payloads use structured-clone semantics and each subscriber receives an independent value, enabling loose coupling without shared mutable state. Functions, promises, weak collections, and host capability objects cannot be published.
|
|
207
|
+
|
|
208
|
+
### Permission
|
|
209
|
+
|
|
210
|
+
Declare `plugin:event-bus` in the manifest to enable the bus:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
permissions: ['plugin:event-bus']
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`ctx.eventBus` is optional and should be feature-detected before use.
|
|
217
|
+
|
|
218
|
+
### API Summary
|
|
219
|
+
|
|
220
|
+
| Method | Description |
|
|
221
|
+
|--------|-------------|
|
|
222
|
+
| `publish(topic, payload?)` | Fire-and-forget message to all current subscribers of the exact topic. |
|
|
223
|
+
| `subscribe(topic, handler)` | Registers a handler; returns an unsubscribe function. |
|
|
224
|
+
|
|
225
|
+
Every message received by a subscriber is a `PluginEventBusMessage`:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
interface PluginEventBusMessage {
|
|
229
|
+
topic: string; // The topic this message was published to
|
|
230
|
+
payload: unknown; // Independent structured-clone value
|
|
231
|
+
timestamp: number; // Epoch ms when the host dispatched the message
|
|
232
|
+
publisher: {
|
|
233
|
+
pluginName: string; // Publishing plugin's name
|
|
234
|
+
instanceScope: 'operator' | 'global';
|
|
235
|
+
operatorId?: string; // Present when the publisher is operator-scoped
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Topic Naming Convention
|
|
241
|
+
|
|
242
|
+
Use dot-separated, plugin-prefixed names to avoid collisions between plugins:
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
<plugin-name>.<domain>.<event>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Examples:
|
|
249
|
+
- `psk-reporter.spot.sent` — a spot was uploaded to PSK Reporter
|
|
250
|
+
- `callsign-filter.match.found` — a callsign matched a filter rule
|
|
251
|
+
- `logbook-sync.upload.complete` — a logbook sync finished
|
|
252
|
+
|
|
253
|
+
Avoid generic names like `update` or `message` — they will collide.
|
|
254
|
+
|
|
255
|
+
### Basic Usage
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { definePlugin } from '@tx5dr/plugin-api';
|
|
259
|
+
|
|
260
|
+
// Publisher plugin
|
|
261
|
+
const publisher = definePlugin({
|
|
262
|
+
apiVersion: 2,
|
|
263
|
+
name: 'spot-monitor',
|
|
264
|
+
version: '1.0.0',
|
|
265
|
+
type: 'utility',
|
|
266
|
+
permissions: ['plugin:event-bus'],
|
|
267
|
+
hooks: {
|
|
268
|
+
onDecode(messages, ctx) {
|
|
269
|
+
for (const msg of messages) {
|
|
270
|
+
ctx.eventBus.publish('spot-monitor.new-spot', {
|
|
271
|
+
callsign: msg.callsign,
|
|
272
|
+
frequency: msg.frequencyHz,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Subscriber plugin
|
|
280
|
+
const subscriber = definePlugin({
|
|
281
|
+
apiVersion: 2,
|
|
282
|
+
name: 'spot-logger',
|
|
283
|
+
version: '1.0.0',
|
|
284
|
+
type: 'utility',
|
|
285
|
+
permissions: ['plugin:event-bus'],
|
|
286
|
+
hooks: {
|
|
287
|
+
onLoad(ctx) {
|
|
288
|
+
ctx.eventBus.subscribe('spot-monitor.new-spot', (message) => {
|
|
289
|
+
ctx.log.info('received spot', {
|
|
290
|
+
from: message.publisher.pluginName,
|
|
291
|
+
callsign: (message.payload as any).callsign,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Cross-Operator Communication
|
|
300
|
+
|
|
301
|
+
Operator-scoped plugins can communicate across operators on the same host. The `publisher` metadata lets subscribers identify which operator sent the message:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
ctx.eventBus.subscribe('qso-monitor.qso-complete', (message) => {
|
|
305
|
+
const { callsign, band } = message.payload as any;
|
|
306
|
+
ctx.log.info('QSO completed by another operator', {
|
|
307
|
+
operator: message.publisher.operatorId,
|
|
308
|
+
callsign,
|
|
309
|
+
band,
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Lifecycle and Error Handling
|
|
315
|
+
|
|
316
|
+
- **Auto-cleanup**: the host removes all subscriptions when a plugin instance unloads. No manual cleanup required.
|
|
317
|
+
- **Manual unsubscribe**: call the function returned by `subscribe()` to cancel a single subscription early.
|
|
318
|
+
- **Error isolation**: subscriber exceptions (sync or async) are captured and logged by the host. They never propagate back to the publisher.
|
|
319
|
+
- **Delivery order**: subscribers receive messages in registration order. Async handlers are awaited, but the publisher does not wait for completion.
|
|
320
|
+
|
|
321
|
+
### Testing
|
|
322
|
+
|
|
323
|
+
Use `createMockEventBus()` from `@tx5dr/plugin-api/testing` to test plugin event bus logic in isolation:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
327
|
+
import { createMockEventBus } from '@tx5dr/plugin-api/testing';
|
|
328
|
+
|
|
329
|
+
it('publishes spot data', () => {
|
|
330
|
+
const bus = createMockEventBus({ owner: { pluginName: 'spot-monitor' } });
|
|
331
|
+
const handler = vi.fn();
|
|
332
|
+
|
|
333
|
+
bus.subscribe('spot-monitor.new-spot', handler);
|
|
334
|
+
bus.publish('spot-monitor.new-spot', { callsign: 'W1AW', frequency: 14074000 });
|
|
335
|
+
|
|
336
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
337
|
+
expect(handler).toHaveBeenCalledWith(expect.objectContaining({
|
|
338
|
+
topic: 'spot-monitor.new-spot',
|
|
339
|
+
payload: { callsign: 'W1AW', frequency: 14074000 },
|
|
340
|
+
publisher: expect.objectContaining({ pluginName: 'spot-monitor' }),
|
|
341
|
+
}));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('tracks published messages', () => {
|
|
345
|
+
const bus = createMockEventBus();
|
|
346
|
+
|
|
347
|
+
bus.publish('topic-a', { value: 1 });
|
|
348
|
+
bus.publish('topic-b', { value: 2 });
|
|
349
|
+
|
|
350
|
+
expect(bus._published).toHaveLength(2);
|
|
351
|
+
expect(bus._published[0].topic).toBe('topic-a');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('unsubscribe prevents further delivery', () => {
|
|
355
|
+
const bus = createMockEventBus();
|
|
356
|
+
const handler = vi.fn();
|
|
357
|
+
|
|
358
|
+
const unsub = bus.subscribe('topic', handler);
|
|
359
|
+
bus.publish('topic', 'first');
|
|
360
|
+
unsub();
|
|
361
|
+
bus.publish('topic', 'second');
|
|
362
|
+
|
|
363
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
The mock records all published messages in `_published` and exposes the internal `_subscriptions` map for advanced inspection.
|
|
368
|
+
|
|
112
369
|
## Bridge SDK Types
|
|
113
370
|
|
|
114
371
|
Plugin iframe pages communicate with the host via the Bridge SDK (`window.tx5dr`), which is automatically injected by the host. To get IDE autocomplete for the Bridge SDK, add the type reference to your project:
|
|
@@ -29,4 +29,44 @@ describe('ADIF QSO mode projection', () => {
|
|
|
29
29
|
expect(parsed?.submode).toBe('USB');
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
|
+
describe('ADIF QSO comments', () => {
|
|
33
|
+
it('exports WSJT-X compatible signal reports in COMMENT', () => {
|
|
34
|
+
const adif = convertQSOToADIF(createQso({
|
|
35
|
+
mode: 'FT8',
|
|
36
|
+
submode: undefined,
|
|
37
|
+
reportSent: '-12',
|
|
38
|
+
reportReceived: '-09',
|
|
39
|
+
}));
|
|
40
|
+
expect(adif).toMatch(/<comment:\d+>FT8 {2}Sent: -12 {2}Rcvd: -09/);
|
|
41
|
+
});
|
|
42
|
+
it('keeps operator comments after the signal report COMMENT prefix', () => {
|
|
43
|
+
const adif = convertQSOToADIF(createQso({
|
|
44
|
+
mode: 'FT8',
|
|
45
|
+
submode: undefined,
|
|
46
|
+
reportSent: '-12',
|
|
47
|
+
reportReceived: '-09',
|
|
48
|
+
comment: 'TU',
|
|
49
|
+
}));
|
|
50
|
+
expect(adif).toMatch(/<comment:\d+>FT8 {2}Sent: -12 {2}Rcvd: -09 \| TU/);
|
|
51
|
+
});
|
|
52
|
+
it('stores message history in a TX-5DR private field instead of COMMENT', () => {
|
|
53
|
+
const adif = convertQSOToADIF(createQso({
|
|
54
|
+
mode: 'FT8',
|
|
55
|
+
submode: undefined,
|
|
56
|
+
reportSent: undefined,
|
|
57
|
+
reportReceived: undefined,
|
|
58
|
+
messageHistory: ['CQ TEST', 'RR73'],
|
|
59
|
+
}));
|
|
60
|
+
expect(adif).toMatch(/<app_tx5dr_message_history:\d+>CQ TEST \| RR73/);
|
|
61
|
+
expect(adif).not.toMatch(/<comment:\d+>CQ TEST/);
|
|
62
|
+
});
|
|
63
|
+
it('parses private message history and legacy COMMENT history', () => {
|
|
64
|
+
const parsedPrivate = parseADIFRecord('<call:6>N0CALL<qso_date:8>20260417<time_on:6>120000<mode:3>FT8<freq:9>14.074000<comment:25>FT8 Sent: -12 Rcvd: -09<app_tx5dr_message_history:14>CQ TEST | RR73<eor>', 'test');
|
|
65
|
+
const parsedLegacy = parseADIFRecord('<call:6>N0CALL<qso_date:8>20260417<time_on:6>120000<mode:3>FT8<freq:9>14.074000<comment:14>CQ TEST | RR73<eor>', 'test');
|
|
66
|
+
expect(parsedPrivate?.comment).toBe('FT8 Sent: -12 Rcvd: -09');
|
|
67
|
+
expect(parsedPrivate?.messageHistory).toEqual(['CQ TEST', 'RR73']);
|
|
68
|
+
expect(parsedLegacy?.comment).toBe('CQ TEST | RR73');
|
|
69
|
+
expect(parsedLegacy?.messageHistory).toEqual(['CQ TEST', 'RR73']);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
32
72
|
//# sourceMappingURL=adif.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adif.test.js","sourceRoot":"","sources":["../../src/__tests__/adif.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGrE,SAAS,SAAS,CAAC,YAAgC,EAAE;IACnD,OAAO;QACL,EAAE,EAAE,WAAW;QACf,QAAQ,EAAE,QAAQ;QAClB,SAAS,EAAE,UAAU;QACrB,IAAI,EAAE,KAAK;QACX,OAAO,EAAE,KAAK;QACd,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC;QACjD,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC;QAC/C,UAAU,EAAE,IAAI;QAChB,cAAc,EAAE,IAAI;QACpB,cAAc,EAAE,EAAE;QAClB,UAAU,EAAE,QAAQ;QACpB,MAAM,EAAE,QAAQ;QAChB,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,IAAI,GAAG,gBAAgB,CAAC,SAAS,EAAE,CAAC,CAAC;QAE3C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;QACtC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,MAAM,GAAG,eAAe,CAC5B,sFAAsF,EACtF,MAAM,CACP,CAAC;QAEF,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"adif.test.js","sourceRoot":"","sources":["../../src/__tests__/adif.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGrE,SAAS,SAAS,CAAC,YAAgC,EAAE;IACnD,OAAO;QACL,EAAE,EAAE,WAAW;QACf,QAAQ,EAAE,QAAQ;QAClB,SAAS,EAAE,UAAU;QACrB,IAAI,EAAE,KAAK;QACX,OAAO,EAAE,KAAK;QACd,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC;QACjD,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC;QAC/C,UAAU,EAAE,IAAI;QAChB,cAAc,EAAE,IAAI;QACpB,cAAc,EAAE,EAAE;QAClB,UAAU,EAAE,QAAQ;QACpB,MAAM,EAAE,QAAQ;QAChB,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,IAAI,GAAG,gBAAgB,CAAC,SAAS,EAAE,CAAC,CAAC;QAE3C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;QACtC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,MAAM,GAAG,eAAe,CAC5B,sFAAsF,EACtF,MAAM,CACP,CAAC;QAEF,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,IAAI,GAAG,gBAAgB,CAAC,SAAS,CAAC;YACtC,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,SAAS;YAClB,UAAU,EAAE,KAAK;YACjB,cAAc,EAAE,KAAK;SACtB,CAAC,CAAC,CAAC;QAEJ,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,4CAA4C,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,IAAI,GAAG,gBAAgB,CAAC,SAAS,CAAC;YACtC,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,SAAS;YAClB,UAAU,EAAE,KAAK;YACjB,cAAc,EAAE,KAAK;YACrB,OAAO,EAAE,IAAI;SACd,CAAC,CAAC,CAAC;QAEJ,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,kDAAkD,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,MAAM,IAAI,GAAG,gBAAgB,CAAC,SAAS,CAAC;YACtC,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,SAAS;YAClB,UAAU,EAAE,SAAS;YACrB,cAAc,EAAE,SAAS;YACzB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC;SACpC,CAAC,CAAC,CAAC;QAEJ,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,gDAAgD,CAAC,CAAC;QACvE,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,aAAa,GAAG,eAAe,CACnC,uKAAuK,EACvK,MAAM,CACP,CAAC;QACF,MAAM,YAAY,GAAG,eAAe,CAClC,gHAAgH,EAChH,MAAM,CACP,CAAC;QAEF,MAAM,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;QACjE,MAAM,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;QACnE,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACrD,MAAM,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability-context.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/capability-context.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it } from 'vitest';
|
|
2
|
+
import { definePlugin } from '../definition.js';
|
|
3
|
+
describe('capability-derived plugin contexts', () => {
|
|
4
|
+
it('keeps speculative strategy factories free of command capabilities', () => {
|
|
5
|
+
definePlugin({
|
|
6
|
+
apiVersion: 2,
|
|
7
|
+
name: 'safe-strategy',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
type: 'strategy',
|
|
10
|
+
permissions: ['operator:transmit-control', 'radio:control'],
|
|
11
|
+
createStrategyRuntime(ctx) {
|
|
12
|
+
// @ts-expect-error strategy factories receive only the speculative read context
|
|
13
|
+
void ctx.operatorCommands;
|
|
14
|
+
// @ts-expect-error strategy factories cannot capture radio command ports
|
|
15
|
+
void ctx.radioCommands;
|
|
16
|
+
throw new Error('type-only fixture');
|
|
17
|
+
},
|
|
18
|
+
isAutoCallEnabled: () => true,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it('keeps unload cleanup free of runtime command capabilities', () => {
|
|
22
|
+
definePlugin({
|
|
23
|
+
apiVersion: 2,
|
|
24
|
+
name: 'safe-cleanup',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
type: 'utility',
|
|
27
|
+
permissions: ['operator:transmit-control', 'radio:control', 'network'],
|
|
28
|
+
onUnload(ctx) {
|
|
29
|
+
void ctx.store.global.flush();
|
|
30
|
+
ctx.timers.clearAll();
|
|
31
|
+
void ctx.files;
|
|
32
|
+
// @ts-expect-error unload cannot submit operator mutations
|
|
33
|
+
void ctx.operatorCommands;
|
|
34
|
+
// @ts-expect-error unload cannot control the physical radio
|
|
35
|
+
void ctx.radioCommands;
|
|
36
|
+
// @ts-expect-error unload cannot retain network capabilities
|
|
37
|
+
void ctx.network;
|
|
38
|
+
},
|
|
39
|
+
isAutoCallEnabled: () => true,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
it('never exposes raw physical transmission primitives, even with command capabilities', () => {
|
|
43
|
+
definePlugin({
|
|
44
|
+
apiVersion: 2,
|
|
45
|
+
name: 'host-arbitrated-transmit-control',
|
|
46
|
+
version: '1.0.0',
|
|
47
|
+
type: 'utility',
|
|
48
|
+
permissions: ['operator:transmit-control', 'radio:read', 'radio:control', 'radio:power'],
|
|
49
|
+
hooks: {
|
|
50
|
+
async onUserAction(_actionId, _payload, ctx) {
|
|
51
|
+
await ctx.operatorCommands.submit({ type: 'stop-automation' });
|
|
52
|
+
await ctx.radioCommands.submit({ type: 'set-frequency', frequency: 14_074_000 });
|
|
53
|
+
// @ts-expect-error physical PTT ownership is never a plugin capability
|
|
54
|
+
void ctx.setPTT;
|
|
55
|
+
// @ts-expect-error audio playback is owned by PhysicalTxCoordinator
|
|
56
|
+
void ctx.playAudio;
|
|
57
|
+
// @ts-expect-error frame mixing is an internal coordinator detail
|
|
58
|
+
void ctx.audioMixer;
|
|
59
|
+
// @ts-expect-error encoding is submitted through host intents only
|
|
60
|
+
void ctx.encoder;
|
|
61
|
+
// @ts-expect-error global emergency stop is reserved for authenticated host commands
|
|
62
|
+
void ctx.forceStopTransmission;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
isAutoCallEnabled: () => true,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
it('requires an explicit tuner capability for tuner RF actions', () => {
|
|
69
|
+
definePlugin({
|
|
70
|
+
apiVersion: 2,
|
|
71
|
+
name: 'frequency-only',
|
|
72
|
+
version: '1.0.0',
|
|
73
|
+
type: 'utility',
|
|
74
|
+
permissions: ['radio:control'],
|
|
75
|
+
hooks: {
|
|
76
|
+
onUserAction(_actionId, _payload, ctx) {
|
|
77
|
+
void ctx.radioCommands.submit({ type: 'set-frequency', frequency: 7_074_000 });
|
|
78
|
+
// @ts-expect-error tuner RF actions require radio:tuner-control
|
|
79
|
+
void ctx.radioTunerCommands;
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
definePlugin({
|
|
84
|
+
apiVersion: 2,
|
|
85
|
+
name: 'tuner-controller',
|
|
86
|
+
version: '1.0.0',
|
|
87
|
+
type: 'utility',
|
|
88
|
+
permissions: ['radio:tuner-control'],
|
|
89
|
+
hooks: {
|
|
90
|
+
onUserAction(_actionId, _payload, ctx) {
|
|
91
|
+
void ctx.radioTunerCommands.submit({ type: 'start-manual-tune' });
|
|
92
|
+
// @ts-expect-error tuner control does not grant frequency control
|
|
93
|
+
void ctx.radioCommands;
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
//# sourceMappingURL=capability-context.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability-context.test.js","sourceRoot":"","sources":["../../src/__tests__/capability-context.test.ts"],"names":[],"mappings":"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"}
|