@firstperson/firstperson 2026.1.33
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/.claude/settings.local.json +16 -0
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/index.ts +18 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +56 -0
- package/src/channel.test.ts +650 -0
- package/src/channel.ts +742 -0
- package/src/config-schema.test.ts +81 -0
- package/src/config-schema.ts +13 -0
- package/src/relay-client.test.ts +452 -0
- package/src/relay-client.ts +266 -0
- package/src/runtime.ts +14 -0
- package/src/types.ts +32 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import type { CoreConfig } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// Mock openclaw/plugin-sdk
|
|
5
|
+
vi.mock("openclaw/plugin-sdk", () => ({
|
|
6
|
+
buildChannelConfigSchema: vi.fn((schema) => schema),
|
|
7
|
+
DEFAULT_ACCOUNT_ID: "default",
|
|
8
|
+
formatPairingApproveHint: vi.fn((channel) => `openclaw pairing approve ${channel} <code>`),
|
|
9
|
+
PAIRING_APPROVED_MESSAGE: "Your device has been approved.",
|
|
10
|
+
setAccountEnabledInConfigSection: vi.fn(({ cfg, sectionKey, enabled }) => ({
|
|
11
|
+
...cfg,
|
|
12
|
+
channels: {
|
|
13
|
+
...cfg.channels,
|
|
14
|
+
[sectionKey]: { ...cfg.channels?.[sectionKey], enabled },
|
|
15
|
+
},
|
|
16
|
+
})),
|
|
17
|
+
deleteAccountFromConfigSection: vi.fn(({ cfg, sectionKey }) => {
|
|
18
|
+
const { [sectionKey]: _, ...rest } = cfg.channels || {};
|
|
19
|
+
return { ...cfg, channels: rest };
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Mock the runtime module
|
|
24
|
+
vi.mock("./runtime.js", () => ({
|
|
25
|
+
getFirstPersonRuntime: vi.fn(() => ({
|
|
26
|
+
config: {
|
|
27
|
+
readConfigFile: vi.fn().mockResolvedValue({}),
|
|
28
|
+
writeConfigFile: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
},
|
|
30
|
+
})),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// Import after mocks are set up
|
|
34
|
+
const { firstPersonPlugin } = await import("./channel.js");
|
|
35
|
+
|
|
36
|
+
describe("firstPersonPlugin", () => {
|
|
37
|
+
const originalEnv = process.env;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.resetModules();
|
|
41
|
+
process.env = { ...originalEnv };
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
process.env = originalEnv;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("meta", () => {
|
|
49
|
+
it("has correct channel metadata", () => {
|
|
50
|
+
expect(firstPersonPlugin.id).toBe("firstperson");
|
|
51
|
+
expect(firstPersonPlugin.meta.label).toBe("First Person");
|
|
52
|
+
expect(firstPersonPlugin.meta.selectionLabel).toBe("First Person (iOS)");
|
|
53
|
+
expect(firstPersonPlugin.meta.docsPath).toBe("/channels/firstperson");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("capabilities", () => {
|
|
58
|
+
it("supports direct messages only", () => {
|
|
59
|
+
expect(firstPersonPlugin.capabilities.chatTypes).toEqual(["direct"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("supports media", () => {
|
|
63
|
+
expect(firstPersonPlugin.capabilities.media).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("does not support threads or reactions", () => {
|
|
67
|
+
expect(firstPersonPlugin.capabilities.threads).toBe(false);
|
|
68
|
+
expect(firstPersonPlugin.capabilities.reactions).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("config adapter", () => {
|
|
73
|
+
describe("listAccountIds", () => {
|
|
74
|
+
it("returns empty array when no config", () => {
|
|
75
|
+
const cfg: CoreConfig = {};
|
|
76
|
+
const result = firstPersonPlugin.config.listAccountIds(cfg);
|
|
77
|
+
expect(result).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns default account when configured", () => {
|
|
81
|
+
const cfg: CoreConfig = {
|
|
82
|
+
channels: {
|
|
83
|
+
firstperson: {
|
|
84
|
+
token: "fp_token_123",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
const result = firstPersonPlugin.config.listAccountIds(cfg);
|
|
89
|
+
expect(result).toEqual(["default"]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("resolveAccount", () => {
|
|
94
|
+
it("resolves account from config token", () => {
|
|
95
|
+
const cfg: CoreConfig = {
|
|
96
|
+
channels: {
|
|
97
|
+
firstperson: {
|
|
98
|
+
token: "fp_token_123",
|
|
99
|
+
relayUrl: "wss://custom.relay.ai",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const account = firstPersonPlugin.config.resolveAccount(cfg, "default");
|
|
105
|
+
|
|
106
|
+
expect(account.accountId).toBe("default");
|
|
107
|
+
expect(account.token).toBe("fp_token_123");
|
|
108
|
+
expect(account.relayUrl).toBe("wss://custom.relay.ai");
|
|
109
|
+
expect(account.tokenSource).toBe("config");
|
|
110
|
+
expect(account.configured).toBe(true);
|
|
111
|
+
expect(account.enabled).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("falls back to env token when no config token", () => {
|
|
115
|
+
process.env.FIRSTPERSON_TOKEN = "fp_env_token";
|
|
116
|
+
|
|
117
|
+
const cfg: CoreConfig = {
|
|
118
|
+
channels: {
|
|
119
|
+
firstperson: {},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const account = firstPersonPlugin.config.resolveAccount(cfg, "default");
|
|
124
|
+
|
|
125
|
+
expect(account.token).toBe("fp_env_token");
|
|
126
|
+
expect(account.tokenSource).toBe("env");
|
|
127
|
+
expect(account.configured).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("uses default relay URL when not specified", () => {
|
|
131
|
+
const cfg: CoreConfig = {
|
|
132
|
+
channels: {
|
|
133
|
+
firstperson: {
|
|
134
|
+
token: "fp_token_123",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const account = firstPersonPlugin.config.resolveAccount(cfg, "default");
|
|
140
|
+
|
|
141
|
+
expect(account.relayUrl).toBe("wss://chat.firstperson.ai");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("respects enabled flag", () => {
|
|
145
|
+
const cfg: CoreConfig = {
|
|
146
|
+
channels: {
|
|
147
|
+
firstperson: {
|
|
148
|
+
enabled: false,
|
|
149
|
+
token: "fp_token_123",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const account = firstPersonPlugin.config.resolveAccount(cfg, "default");
|
|
155
|
+
|
|
156
|
+
expect(account.enabled).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("returns unconfigured when no token", () => {
|
|
160
|
+
const cfg: CoreConfig = {
|
|
161
|
+
channels: {
|
|
162
|
+
firstperson: {},
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const account = firstPersonPlugin.config.resolveAccount(cfg, "default");
|
|
167
|
+
|
|
168
|
+
expect(account.token).toBeNull();
|
|
169
|
+
expect(account.tokenSource).toBe("none");
|
|
170
|
+
expect(account.configured).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("isConfigured", () => {
|
|
175
|
+
it("returns true when account has token", () => {
|
|
176
|
+
const account = {
|
|
177
|
+
accountId: "default",
|
|
178
|
+
configured: true,
|
|
179
|
+
token: "fp_token_123",
|
|
180
|
+
tokenSource: "config" as const,
|
|
181
|
+
enabled: true,
|
|
182
|
+
relayUrl: "wss://chat.firstperson.ai",
|
|
183
|
+
config: {},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
expect(firstPersonPlugin.config.isConfigured(account)).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("returns false when account has no token", () => {
|
|
190
|
+
const account = {
|
|
191
|
+
accountId: "default",
|
|
192
|
+
configured: false,
|
|
193
|
+
token: null,
|
|
194
|
+
tokenSource: "none" as const,
|
|
195
|
+
enabled: true,
|
|
196
|
+
relayUrl: "wss://chat.firstperson.ai",
|
|
197
|
+
config: {},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
expect(firstPersonPlugin.config.isConfigured(account)).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("resolveAllowFrom", () => {
|
|
205
|
+
it("returns allowFrom array from config", () => {
|
|
206
|
+
const cfg: CoreConfig = {
|
|
207
|
+
channels: {
|
|
208
|
+
firstperson: {
|
|
209
|
+
allowFrom: ["device-1", "device-2"],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const result = firstPersonPlugin.config.resolveAllowFrom!({ cfg });
|
|
215
|
+
expect(result).toEqual(["device-1", "device-2"]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("returns empty array when no allowFrom", () => {
|
|
219
|
+
const cfg: CoreConfig = {
|
|
220
|
+
channels: {
|
|
221
|
+
firstperson: {},
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const result = firstPersonPlugin.config.resolveAllowFrom!({ cfg });
|
|
226
|
+
expect(result).toEqual([]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("converts numbers to strings", () => {
|
|
230
|
+
const cfg: CoreConfig = {
|
|
231
|
+
channels: {
|
|
232
|
+
firstperson: {
|
|
233
|
+
allowFrom: [123, "device-1"],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const result = firstPersonPlugin.config.resolveAllowFrom!({ cfg });
|
|
239
|
+
expect(result).toEqual(["123", "device-1"]);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("formatAllowFrom", () => {
|
|
244
|
+
it("normalizes and filters entries", () => {
|
|
245
|
+
const result = firstPersonPlugin.config.formatAllowFrom!({
|
|
246
|
+
allowFrom: [" Device-1 ", "DEVICE-2", "", " "],
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(result).toEqual(["device-1", "device-2"]);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("security adapter", () => {
|
|
255
|
+
describe("resolveDmPolicy", () => {
|
|
256
|
+
it("defaults to pairing policy", () => {
|
|
257
|
+
const account = {
|
|
258
|
+
accountId: "default",
|
|
259
|
+
configured: true,
|
|
260
|
+
token: "fp_token_123",
|
|
261
|
+
tokenSource: "config" as const,
|
|
262
|
+
enabled: true,
|
|
263
|
+
relayUrl: "wss://chat.firstperson.ai",
|
|
264
|
+
config: {},
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const result = firstPersonPlugin.security!.resolveDmPolicy({ account });
|
|
268
|
+
|
|
269
|
+
expect(result.policy).toBe("pairing");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("uses configured policy", () => {
|
|
273
|
+
const account = {
|
|
274
|
+
accountId: "default",
|
|
275
|
+
configured: true,
|
|
276
|
+
token: "fp_token_123",
|
|
277
|
+
tokenSource: "config" as const,
|
|
278
|
+
enabled: true,
|
|
279
|
+
relayUrl: "wss://chat.firstperson.ai",
|
|
280
|
+
config: {
|
|
281
|
+
dmPolicy: "allowlist" as const,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const result = firstPersonPlugin.security!.resolveDmPolicy({ account });
|
|
286
|
+
|
|
287
|
+
expect(result.policy).toBe("allowlist");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("includes allowFrom from config", () => {
|
|
291
|
+
const account = {
|
|
292
|
+
accountId: "default",
|
|
293
|
+
configured: true,
|
|
294
|
+
token: "fp_token_123",
|
|
295
|
+
tokenSource: "config" as const,
|
|
296
|
+
enabled: true,
|
|
297
|
+
relayUrl: "wss://chat.firstperson.ai",
|
|
298
|
+
config: {
|
|
299
|
+
allowFrom: ["device-1", "device-2"],
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const result = firstPersonPlugin.security!.resolveDmPolicy({ account });
|
|
304
|
+
|
|
305
|
+
expect(result.allowFrom).toEqual(["device-1", "device-2"]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("normalizes entries correctly", () => {
|
|
309
|
+
const account = {
|
|
310
|
+
accountId: "default",
|
|
311
|
+
configured: true,
|
|
312
|
+
token: "fp_token_123",
|
|
313
|
+
tokenSource: "config" as const,
|
|
314
|
+
enabled: true,
|
|
315
|
+
relayUrl: "wss://chat.firstperson.ai",
|
|
316
|
+
config: {},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const result = firstPersonPlugin.security!.resolveDmPolicy({ account });
|
|
320
|
+
|
|
321
|
+
expect(result.normalizeEntry(" firstperson:Device-1 ")).toBe("device-1");
|
|
322
|
+
expect(result.normalizeEntry("FIRSTPERSON:DEVICE-2")).toBe("device-2");
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("pairing adapter", () => {
|
|
328
|
+
it("normalizes allow entries", () => {
|
|
329
|
+
const normalized = firstPersonPlugin.pairing!.normalizeAllowEntry("firstperson:Device-123");
|
|
330
|
+
expect(normalized).toBe("device-123");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("handles entries without prefix", () => {
|
|
334
|
+
const normalized = firstPersonPlugin.pairing!.normalizeAllowEntry(" DEVICE-456 ");
|
|
335
|
+
expect(normalized).toBe("device-456");
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("messaging adapter", () => {
|
|
340
|
+
describe("normalizeTarget", () => {
|
|
341
|
+
it("normalizes target to lowercase", () => {
|
|
342
|
+
const result = firstPersonPlugin.messaging!.normalizeTarget("DEVICE-123");
|
|
343
|
+
expect(result).toBe("device-123");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("trims whitespace", () => {
|
|
347
|
+
const result = firstPersonPlugin.messaging!.normalizeTarget(" device-123 ");
|
|
348
|
+
expect(result).toBe("device-123");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("returns undefined for empty input", () => {
|
|
352
|
+
const result = firstPersonPlugin.messaging!.normalizeTarget("");
|
|
353
|
+
expect(result).toBeUndefined();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("returns undefined for undefined input", () => {
|
|
357
|
+
const result = firstPersonPlugin.messaging!.normalizeTarget(undefined as unknown as string);
|
|
358
|
+
expect(result).toBeUndefined();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe("targetResolver", () => {
|
|
363
|
+
it("identifies valid device IDs", () => {
|
|
364
|
+
expect(firstPersonPlugin.messaging!.targetResolver!.looksLikeId("device-123")).toBe(true);
|
|
365
|
+
expect(firstPersonPlugin.messaging!.targetResolver!.looksLikeId("abc")).toBe(true);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("rejects empty IDs", () => {
|
|
369
|
+
expect(firstPersonPlugin.messaging!.targetResolver!.looksLikeId("")).toBe(false);
|
|
370
|
+
expect(firstPersonPlugin.messaging!.targetResolver!.looksLikeId(" ")).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("provides correct hint", () => {
|
|
374
|
+
expect(firstPersonPlugin.messaging!.targetResolver!.hint).toBe("<deviceId>");
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("directory adapter", () => {
|
|
380
|
+
it("lists peers from allowFrom", async () => {
|
|
381
|
+
const cfg: CoreConfig = {
|
|
382
|
+
channels: {
|
|
383
|
+
firstperson: {
|
|
384
|
+
allowFrom: ["device-1", "device-2"],
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const peers = await firstPersonPlugin.directory!.listPeers!({ cfg });
|
|
390
|
+
|
|
391
|
+
expect(peers).toEqual([
|
|
392
|
+
{ kind: "user", id: "device-1" },
|
|
393
|
+
{ kind: "user", id: "device-2" },
|
|
394
|
+
]);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("returns empty array when no allowFrom", async () => {
|
|
398
|
+
const cfg: CoreConfig = {
|
|
399
|
+
channels: {
|
|
400
|
+
firstperson: {},
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const peers = await firstPersonPlugin.directory!.listPeers!({ cfg });
|
|
405
|
+
expect(peers).toEqual([]);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("filters empty entries", async () => {
|
|
409
|
+
const cfg: CoreConfig = {
|
|
410
|
+
channels: {
|
|
411
|
+
firstperson: {
|
|
412
|
+
allowFrom: ["device-1", "", " ", "device-2"],
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const peers = await firstPersonPlugin.directory!.listPeers!({ cfg });
|
|
418
|
+
|
|
419
|
+
expect(peers).toEqual([
|
|
420
|
+
{ kind: "user", id: "device-1" },
|
|
421
|
+
{ kind: "user", id: "device-2" },
|
|
422
|
+
]);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("returns empty groups array", async () => {
|
|
426
|
+
const groups = await firstPersonPlugin.directory!.listGroups!({ cfg: {} });
|
|
427
|
+
expect(groups).toEqual([]);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("returns null for self", async () => {
|
|
431
|
+
const self = await firstPersonPlugin.directory!.self!({ cfg: {} });
|
|
432
|
+
expect(self).toBeNull();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe("setup adapter", () => {
|
|
437
|
+
describe("validateInput", () => {
|
|
438
|
+
it("accepts valid token", () => {
|
|
439
|
+
const result = firstPersonPlugin.setup!.validateInput!({
|
|
440
|
+
input: { token: "fp_token_123" },
|
|
441
|
+
} as any);
|
|
442
|
+
expect(result).toBeNull();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("rejects empty token when not using env", () => {
|
|
446
|
+
const result = firstPersonPlugin.setup!.validateInput!({
|
|
447
|
+
input: { token: "" },
|
|
448
|
+
} as any);
|
|
449
|
+
expect(result).toContain("requires a relay token");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("accepts useEnv when env var is set", () => {
|
|
453
|
+
process.env.FIRSTPERSON_TOKEN = "fp_env_token";
|
|
454
|
+
|
|
455
|
+
const result = firstPersonPlugin.setup!.validateInput!({
|
|
456
|
+
input: { useEnv: true },
|
|
457
|
+
} as any);
|
|
458
|
+
expect(result).toBeNull();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("rejects useEnv when env var is not set", () => {
|
|
462
|
+
delete process.env.FIRSTPERSON_TOKEN;
|
|
463
|
+
|
|
464
|
+
const result = firstPersonPlugin.setup!.validateInput!({
|
|
465
|
+
input: { useEnv: true },
|
|
466
|
+
} as any);
|
|
467
|
+
expect(result).toContain("FIRSTPERSON_TOKEN environment variable not set");
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
describe("applyAccountConfig", () => {
|
|
472
|
+
it("applies token to config", () => {
|
|
473
|
+
const cfg: CoreConfig = {};
|
|
474
|
+
const result = firstPersonPlugin.setup!.applyAccountConfig!({
|
|
475
|
+
cfg,
|
|
476
|
+
input: { token: "fp_token_123" },
|
|
477
|
+
} as any);
|
|
478
|
+
|
|
479
|
+
expect(result.channels?.firstperson?.token).toBe("fp_token_123");
|
|
480
|
+
expect(result.channels?.firstperson?.enabled).toBe(true);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("applies relayUrl to config", () => {
|
|
484
|
+
const cfg: CoreConfig = {};
|
|
485
|
+
const result = firstPersonPlugin.setup!.applyAccountConfig!({
|
|
486
|
+
cfg,
|
|
487
|
+
input: { token: "fp_token_123", relayUrl: "wss://custom.relay.ai" },
|
|
488
|
+
} as any);
|
|
489
|
+
|
|
490
|
+
expect(result.channels?.firstperson?.relayUrl).toBe("wss://custom.relay.ai");
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("does not write token when using env", () => {
|
|
494
|
+
const cfg: CoreConfig = {};
|
|
495
|
+
const result = firstPersonPlugin.setup!.applyAccountConfig!({
|
|
496
|
+
cfg,
|
|
497
|
+
input: { useEnv: true },
|
|
498
|
+
} as any);
|
|
499
|
+
|
|
500
|
+
expect(result.channels?.firstperson?.token).toBeUndefined();
|
|
501
|
+
expect(result.channels?.firstperson?.enabled).toBe(true);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("preserves existing config", () => {
|
|
505
|
+
const cfg: CoreConfig = {
|
|
506
|
+
channels: {
|
|
507
|
+
firstperson: {
|
|
508
|
+
dmPolicy: "allowlist",
|
|
509
|
+
allowFrom: ["existing-device"],
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const result = firstPersonPlugin.setup!.applyAccountConfig!({
|
|
515
|
+
cfg,
|
|
516
|
+
input: { token: "fp_token_123" },
|
|
517
|
+
} as any);
|
|
518
|
+
|
|
519
|
+
expect(result.channels?.firstperson?.dmPolicy).toBe("allowlist");
|
|
520
|
+
expect(result.channels?.firstperson?.allowFrom).toEqual(["existing-device"]);
|
|
521
|
+
expect(result.channels?.firstperson?.token).toBe("fp_token_123");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
describe("status adapter", () => {
|
|
527
|
+
it("collects config issues when not configured", () => {
|
|
528
|
+
const accounts = [
|
|
529
|
+
{
|
|
530
|
+
accountId: "default",
|
|
531
|
+
configured: false,
|
|
532
|
+
token: null,
|
|
533
|
+
tokenSource: "none" as const,
|
|
534
|
+
enabled: true,
|
|
535
|
+
relayUrl: "wss://chat.firstperson.ai",
|
|
536
|
+
config: {},
|
|
537
|
+
},
|
|
538
|
+
];
|
|
539
|
+
|
|
540
|
+
const issues = firstPersonPlugin.status!.collectStatusIssues!(accounts);
|
|
541
|
+
|
|
542
|
+
expect(issues).toHaveLength(1);
|
|
543
|
+
expect(issues[0]).toEqual({
|
|
544
|
+
channel: "firstperson",
|
|
545
|
+
accountId: "default",
|
|
546
|
+
kind: "config",
|
|
547
|
+
message: "First Person token not configured",
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("returns no issues when configured", () => {
|
|
552
|
+
const accounts = [
|
|
553
|
+
{
|
|
554
|
+
accountId: "default",
|
|
555
|
+
configured: true,
|
|
556
|
+
token: "fp_token_123",
|
|
557
|
+
tokenSource: "config" as const,
|
|
558
|
+
enabled: true,
|
|
559
|
+
relayUrl: "wss://chat.firstperson.ai",
|
|
560
|
+
config: {},
|
|
561
|
+
},
|
|
562
|
+
];
|
|
563
|
+
|
|
564
|
+
const issues = firstPersonPlugin.status!.collectStatusIssues!(accounts);
|
|
565
|
+
expect(issues).toHaveLength(0);
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
describe("outbound adapter", () => {
|
|
570
|
+
it("has correct delivery mode", () => {
|
|
571
|
+
expect(firstPersonPlugin.outbound!.deliveryMode).toBe("direct");
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("has correct text chunk limit", () => {
|
|
575
|
+
expect(firstPersonPlugin.outbound!.textChunkLimit).toBe(4096);
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
describe("onboarding adapter", () => {
|
|
580
|
+
describe("dmPolicy", () => {
|
|
581
|
+
it("gets current policy defaulting to pairing", () => {
|
|
582
|
+
const cfg: CoreConfig = {};
|
|
583
|
+
const result = firstPersonPlugin.onboarding!.dmPolicy!.getCurrent(cfg);
|
|
584
|
+
expect(result).toBe("pairing");
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("gets configured policy", () => {
|
|
588
|
+
const cfg: CoreConfig = {
|
|
589
|
+
channels: {
|
|
590
|
+
firstperson: {
|
|
591
|
+
dmPolicy: "allowlist",
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
const result = firstPersonPlugin.onboarding!.dmPolicy!.getCurrent(cfg);
|
|
596
|
+
expect(result).toBe("allowlist");
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it("sets policy and adds wildcard for open", () => {
|
|
600
|
+
const cfg: CoreConfig = {};
|
|
601
|
+
const result = firstPersonPlugin.onboarding!.dmPolicy!.setPolicy(cfg, "open");
|
|
602
|
+
|
|
603
|
+
expect(result.channels?.firstperson?.dmPolicy).toBe("open");
|
|
604
|
+
expect(result.channels?.firstperson?.allowFrom).toContain("*");
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("sets policy without wildcard for non-open", () => {
|
|
608
|
+
const cfg: CoreConfig = {};
|
|
609
|
+
const result = firstPersonPlugin.onboarding!.dmPolicy!.setPolicy(cfg, "pairing");
|
|
610
|
+
|
|
611
|
+
expect(result.channels?.firstperson?.dmPolicy).toBe("pairing");
|
|
612
|
+
expect(result.channels?.firstperson?.allowFrom).toBeUndefined();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("deduplicates wildcard when already present", () => {
|
|
616
|
+
const cfg: CoreConfig = {
|
|
617
|
+
channels: {
|
|
618
|
+
firstperson: {
|
|
619
|
+
allowFrom: ["*", "device-1"],
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
const result = firstPersonPlugin.onboarding!.dmPolicy!.setPolicy(cfg, "open");
|
|
624
|
+
|
|
625
|
+
const wildcardCount = result.channels?.firstperson?.allowFrom?.filter(
|
|
626
|
+
(v: string | number) => v === "*"
|
|
627
|
+
).length;
|
|
628
|
+
expect(wildcardCount).toBe(1);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
describe("disable", () => {
|
|
633
|
+
it("sets enabled to false", () => {
|
|
634
|
+
const cfg: CoreConfig = {
|
|
635
|
+
channels: {
|
|
636
|
+
firstperson: {
|
|
637
|
+
enabled: true,
|
|
638
|
+
token: "fp_token_123",
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const result = firstPersonPlugin.onboarding!.disable!(cfg);
|
|
644
|
+
|
|
645
|
+
expect(result.channels?.firstperson?.enabled).toBe(false);
|
|
646
|
+
expect(result.channels?.firstperson?.token).toBe("fp_token_123");
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
});
|