@elizaos/plugin-xr 2.0.3-beta.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.
- package/AGENTS.md +151 -0
- package/CLAUDE.md +151 -0
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/package.json +57 -0
- package/simulator/bun.lock +159 -0
- package/simulator/package.json +28 -0
- package/simulator/src/emulator.ts +174 -0
- package/simulator/src/mock-agent.ts +233 -0
- package/simulator/src/node.ts +9 -0
- package/simulator/src/playwright-fixture.ts +169 -0
- package/simulator/src/types.ts +51 -0
- package/simulator/tsconfig.json +13 -0
- package/simulator/vite.config.ts +25 -0
- package/src/__tests__/audio-pipeline.test.ts +129 -0
- package/src/__tests__/protocol.test.ts +53 -0
- package/src/__tests__/routes-e2e.test.ts +276 -0
- package/src/__tests__/vision-pipeline.test.ts +73 -0
- package/src/__tests__/xr-bundle-coverage.test.ts +303 -0
- package/src/__tests__/xr-feature-parity.test.ts +524 -0
- package/src/__tests__/xr-functional-parity.test.ts +522 -0
- package/src/__tests__/xr-view-host-http.test.ts +239 -0
- package/src/__tests__/xr-view-host.test.ts +174 -0
- package/src/actions/xr-query-vision.ts +64 -0
- package/src/actions/xr-view-actions.ts +386 -0
- package/src/index.ts +55 -0
- package/src/protocol.ts +126 -0
- package/src/providers/xr-context.ts +49 -0
- package/src/routes/xr-connect.ts +89 -0
- package/src/routes/xr-simulator-route.ts +37 -0
- package/src/routes/xr-status.ts +36 -0
- package/src/routes/xr-view-host.ts +359 -0
- package/src/routes/xr-views.ts +43 -0
- package/src/services/audio-pipeline.ts +120 -0
- package/src/services/vision-pipeline.ts +57 -0
- package/src/services/xr-session-service.ts +388 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +30 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature-by-feature functional parity validation for all 18 XR views.
|
|
3
|
+
*
|
|
4
|
+
* The architectural guarantee: every XR view uses the SAME bundlePath
|
|
5
|
+
* ("dist/views/bundle.js") and the SAME componentExport as the GUI view.
|
|
6
|
+
* The XR shell (xr-view-host) loads that bundle via dynamic import — making
|
|
7
|
+
* XR and GUI views share 100% of the same React component source.
|
|
8
|
+
*
|
|
9
|
+
* This test suite validates that guarantee explicitly and then verifies
|
|
10
|
+
* the functional content of each component:
|
|
11
|
+
*
|
|
12
|
+
* A. XR views share the same bundle + component as GUI views (structural)
|
|
13
|
+
* B. Each component's source contains the functional UI elements it claims
|
|
14
|
+
* C. The built bundle contains the exported component symbol
|
|
15
|
+
* D. Agent-facing TUI capabilities are present in the shared source
|
|
16
|
+
* (proving the agent sees the same interface in XR as in TUI/GUI)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
20
|
+
import { dirname, resolve } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { describe, expect, it } from "vitest";
|
|
23
|
+
|
|
24
|
+
const repoRoot = resolve(
|
|
25
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
26
|
+
"../../../..",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
function readFile(relPath: string): string {
|
|
30
|
+
return readFileSync(resolve(repoRoot, relPath), "utf8");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fileExists(relPath: string): boolean {
|
|
34
|
+
return existsSync(resolve(repoRoot, relPath));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A view "component" is no longer a single monolithic .tsx — each view is a
|
|
39
|
+
* co-located family of files sharing one directory: the entry `X.tsx`, its
|
|
40
|
+
* agent-facing capability handlers in `X.interact.ts`, data/helpers in
|
|
41
|
+
* `X.helpers.ts`, plus extracted sub-components and hooks. The functional
|
|
42
|
+
* content, React hooks, and TUI capabilities live across that family. Read the
|
|
43
|
+
* whole directory (non-recursive) so the parity checks see the real source the
|
|
44
|
+
* shared bundle is built from, not just the thin shell entry file.
|
|
45
|
+
*/
|
|
46
|
+
function readComponentFamily(relPath: string): string {
|
|
47
|
+
const fileDir = dirname(resolve(repoRoot, relPath));
|
|
48
|
+
if (!existsSync(fileDir)) return "";
|
|
49
|
+
const parts: string[] = [];
|
|
50
|
+
for (const name of readdirSync(fileDir)) {
|
|
51
|
+
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) continue;
|
|
52
|
+
parts.push(readFileSync(resolve(fileDir, name), "utf8"));
|
|
53
|
+
}
|
|
54
|
+
return parts.join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Manifest parser ───────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
interface ViewEntry {
|
|
60
|
+
id: string;
|
|
61
|
+
label: string;
|
|
62
|
+
viewType: "gui" | "tui" | "xr";
|
|
63
|
+
bundlePath: string;
|
|
64
|
+
componentExport: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseViewEntries(source: string): ViewEntry[] {
|
|
68
|
+
const entries: ViewEntry[] = [];
|
|
69
|
+
const viewsStart = source.indexOf("views:");
|
|
70
|
+
if (viewsStart === -1) return entries;
|
|
71
|
+
const arrayStart = source.indexOf("[", viewsStart);
|
|
72
|
+
if (arrayStart === -1) return entries;
|
|
73
|
+
let depth = 0;
|
|
74
|
+
let arrayEnd = -1;
|
|
75
|
+
for (let i = arrayStart; i < source.length; i++) {
|
|
76
|
+
if (source[i] === "[") depth++;
|
|
77
|
+
if (source[i] === "]") depth--;
|
|
78
|
+
if (depth === 0) {
|
|
79
|
+
arrayEnd = i;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (arrayEnd === -1) return entries;
|
|
84
|
+
const body = source.slice(arrayStart + 1, arrayEnd);
|
|
85
|
+
const objects: string[] = [];
|
|
86
|
+
let start = -1;
|
|
87
|
+
depth = 0;
|
|
88
|
+
for (let i = 0; i < body.length; i++) {
|
|
89
|
+
if (body[i] === "{") {
|
|
90
|
+
if (depth === 0) start = i;
|
|
91
|
+
depth++;
|
|
92
|
+
}
|
|
93
|
+
if (body[i] === "}") {
|
|
94
|
+
depth--;
|
|
95
|
+
if (depth === 0 && start !== -1) {
|
|
96
|
+
objects.push(body.slice(start, i + 1));
|
|
97
|
+
start = -1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const obj of objects) {
|
|
102
|
+
const id = obj.match(/\bid:\s*"([^"]+)"/)?.[1];
|
|
103
|
+
const label = obj.match(/label:\s*"([^"]+)"/)?.[1];
|
|
104
|
+
const bundlePath = obj.match(/bundlePath:\s*"([^"]+)"/)?.[1];
|
|
105
|
+
const componentExport = obj.match(/componentExport:\s*"([^"]+)"/)?.[1];
|
|
106
|
+
if (!id || !label || !bundlePath || !componentExport) continue;
|
|
107
|
+
|
|
108
|
+
// A single declaration may draw several surfaces via `modalities:
|
|
109
|
+
// ["gui","xr","tui"]` (the collapsed one-source pattern) instead of a
|
|
110
|
+
// duplicate declaration per `viewType`. Either form yields one ViewEntry
|
|
111
|
+
// per surface, sharing the same bundle + component — so the GUI=XR parity
|
|
112
|
+
// checks hold trivially for the collapsed form (it IS the same declaration).
|
|
113
|
+
const modalitiesMatch = obj.match(
|
|
114
|
+
/modalities:\s*([A-Za-z0-9_]+|\[[^\]]*\])/,
|
|
115
|
+
);
|
|
116
|
+
const modalityLiterals = modalitiesMatch
|
|
117
|
+
? [...modalitiesMatch[1].matchAll(/"(gui|tui|xr)"/g)].map((m) => m[1])
|
|
118
|
+
: [];
|
|
119
|
+
const viewTypes =
|
|
120
|
+
modalityLiterals.length > 0
|
|
121
|
+
? modalityLiterals
|
|
122
|
+
: [obj.match(/viewType:\s*"([^"]+)"/)?.[1] ?? "gui"];
|
|
123
|
+
for (const viewType of viewTypes) {
|
|
124
|
+
entries.push({
|
|
125
|
+
id,
|
|
126
|
+
label,
|
|
127
|
+
viewType: viewType as "gui" | "tui" | "xr",
|
|
128
|
+
bundlePath,
|
|
129
|
+
componentExport,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return entries;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Plugin manifest registry ──────────────────────────────────────────────────
|
|
137
|
+
// Each entry: (plugin directory, manifest path, component source file for XR view)
|
|
138
|
+
|
|
139
|
+
const PLUGIN_REGISTRY: Array<{
|
|
140
|
+
pluginDir: string;
|
|
141
|
+
manifestPath: string;
|
|
142
|
+
/** Path to the component source file the XR view renders */
|
|
143
|
+
xrComponentSrc: string;
|
|
144
|
+
/** Key functional terms that MUST appear in the component source */
|
|
145
|
+
requiredTerms: string[];
|
|
146
|
+
}> = [
|
|
147
|
+
{
|
|
148
|
+
pluginDir: "plugins/plugin-companion",
|
|
149
|
+
manifestPath: "plugins/plugin-companion/src/plugin.ts",
|
|
150
|
+
xrComponentSrc:
|
|
151
|
+
"plugins/plugin-companion/src/components/companion/CompanionView.tsx",
|
|
152
|
+
requiredTerms: ["CompanionView", "CompanionSceneHost", "EmotePicker"],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
pluginDir: "plugins/plugin-contacts",
|
|
156
|
+
manifestPath: "plugins/plugin-contacts/src/plugin.ts",
|
|
157
|
+
xrComponentSrc:
|
|
158
|
+
"plugins/plugin-contacts/src/components/ContactsAppView.tsx",
|
|
159
|
+
requiredTerms: [
|
|
160
|
+
"Contacts",
|
|
161
|
+
"createContact",
|
|
162
|
+
"ContactsAppView",
|
|
163
|
+
"Input",
|
|
164
|
+
"Button",
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
pluginDir: "plugins/plugin-hyperliquid-app",
|
|
169
|
+
manifestPath: "plugins/plugin-hyperliquid-app/src/plugin.ts",
|
|
170
|
+
xrComponentSrc: "plugins/plugin-hyperliquid-app/src/HyperliquidAppView.tsx",
|
|
171
|
+
requiredTerms: ["HyperliquidAppView", "useState"],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
pluginDir: "plugins/plugin-messages",
|
|
175
|
+
manifestPath: "plugins/plugin-messages/src/plugin.ts",
|
|
176
|
+
xrComponentSrc:
|
|
177
|
+
"plugins/plugin-messages/src/components/MessagesAppView.tsx",
|
|
178
|
+
requiredTerms: ["MessagesAppView", "Button"],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
pluginDir: "plugins/app-model-tester",
|
|
182
|
+
manifestPath: "plugins/app-model-tester/src/plugin.ts",
|
|
183
|
+
xrComponentSrc: "plugins/app-model-tester/src/ModelTesterAppView.tsx",
|
|
184
|
+
requiredTerms: ["ModelTesterAppView", "useState"],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
pluginDir: "plugins/plugin-phone",
|
|
188
|
+
manifestPath: "plugins/plugin-phone/src/plugin.ts",
|
|
189
|
+
xrComponentSrc: "plugins/plugin-phone/src/components/PhoneAppView.tsx",
|
|
190
|
+
requiredTerms: ["PhoneAppView", "Phone", "Button", "Tabs"],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
pluginDir: "plugins/plugin-polymarket-app",
|
|
194
|
+
manifestPath: "plugins/plugin-polymarket-app/src/plugin.ts",
|
|
195
|
+
xrComponentSrc: "plugins/plugin-polymarket-app/src/PolymarketAppView.tsx",
|
|
196
|
+
requiredTerms: ["PolymarketAppView", "useState"],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
pluginDir: "plugins/plugin-shopify-ui",
|
|
200
|
+
manifestPath: "plugins/plugin-shopify-ui/src/plugin.ts",
|
|
201
|
+
xrComponentSrc: "plugins/plugin-shopify-ui/src/ShopifyAppView.tsx",
|
|
202
|
+
requiredTerms: ["ShopifyAppView", "useState"],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
pluginDir: "plugins/plugin-steward-app",
|
|
206
|
+
manifestPath: "plugins/plugin-steward-app/src/plugin.ts",
|
|
207
|
+
xrComponentSrc: "plugins/plugin-steward-app/src/StewardView.tsx",
|
|
208
|
+
requiredTerms: ["StewardView", "useState"],
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
pluginDir: "plugins/plugin-vincent",
|
|
212
|
+
manifestPath: "plugins/plugin-vincent/src/plugin.ts",
|
|
213
|
+
xrComponentSrc: "plugins/plugin-vincent/src/VincentAppView.tsx",
|
|
214
|
+
requiredTerms: ["VincentAppView", "useState"],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
pluginDir: "plugins/plugin-wallet-ui",
|
|
218
|
+
manifestPath: "plugins/plugin-wallet-ui/src/plugin.ts",
|
|
219
|
+
xrComponentSrc: "plugins/plugin-wallet-ui/src/InventoryView.tsx",
|
|
220
|
+
requiredTerms: ["InventoryView", "useInventoryData"],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
pluginDir: "plugins/plugin-feed",
|
|
224
|
+
manifestPath: "plugins/plugin-feed/src/index.ts",
|
|
225
|
+
xrComponentSrc: "plugins/plugin-feed/src/ui/FeedOperatorSurface.tsx",
|
|
226
|
+
requiredTerms: ["FeedOperatorSurface", "useState"],
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
pluginDir: "plugins/plugin-app-control",
|
|
230
|
+
manifestPath: "plugins/plugin-app-control/src/index.ts",
|
|
231
|
+
xrComponentSrc: "plugins/plugin-app-control/src/views/ViewManagerView.tsx",
|
|
232
|
+
requiredTerms: ["ViewManagerView", "useState"],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
pluginDir: "plugins/plugin-screenshare",
|
|
236
|
+
manifestPath: "plugins/plugin-screenshare/src/index.ts",
|
|
237
|
+
xrComponentSrc:
|
|
238
|
+
"plugins/plugin-screenshare/src/ui/ScreenshareOperatorSurface.tsx",
|
|
239
|
+
requiredTerms: ["ScreenshareOperatorSurface", "useState"],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
pluginDir: "plugins/plugin-task-coordinator",
|
|
243
|
+
manifestPath: "plugins/plugin-task-coordinator/src/index.ts",
|
|
244
|
+
xrComponentSrc:
|
|
245
|
+
"plugins/plugin-task-coordinator/src/CodingAgentTasksPanel.tsx",
|
|
246
|
+
requiredTerms: ["CodingAgentTasksPanel", "useState"],
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
pluginDir: "plugins/plugin-trajectory-logger",
|
|
250
|
+
manifestPath: "plugins/plugin-trajectory-logger/src/plugin.ts",
|
|
251
|
+
xrComponentSrc:
|
|
252
|
+
"plugins/plugin-trajectory-logger/src/components/TrajectoryLoggerView.tsx",
|
|
253
|
+
requiredTerms: ["TrajectoryLoggerView", "useState"],
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
pluginDir: "plugins/plugin-training",
|
|
257
|
+
manifestPath: "plugins/plugin-training/src/setup-routes.ts",
|
|
258
|
+
xrComponentSrc: "plugins/plugin-training/src/ui/FineTuningView.tsx",
|
|
259
|
+
requiredTerms: ["FineTuningView", "useState"],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
pluginDir: "plugins/plugin-facewear",
|
|
263
|
+
manifestPath: "plugins/plugin-facewear/src/index.ts",
|
|
264
|
+
xrComponentSrc: "plugins/plugin-facewear/src/ui/SmartglassesView.tsx",
|
|
265
|
+
requiredTerms: ["SmartglassesView", "useState"],
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
// ── TUI capability baseline (from plugin-tui-view-coverage.test.ts) ──────────
|
|
270
|
+
// These are the exact agent-facing capabilities each view must expose in its
|
|
271
|
+
// source — proving the same capabilities are available in XR (same component).
|
|
272
|
+
|
|
273
|
+
const TUI_CAPABILITY_SOURCE_MAP: Record<
|
|
274
|
+
string,
|
|
275
|
+
{ srcFile: string; capabilities: string[] }
|
|
276
|
+
> = {
|
|
277
|
+
"plugins/plugin-companion": {
|
|
278
|
+
srcFile:
|
|
279
|
+
"plugins/plugin-companion/src/components/companion/CompanionView.interact.ts",
|
|
280
|
+
capabilities: ["terminal-companion-state", "terminal-companion-emotes"],
|
|
281
|
+
},
|
|
282
|
+
"plugins/plugin-contacts": {
|
|
283
|
+
srcFile:
|
|
284
|
+
"plugins/plugin-contacts/src/components/ContactsAppView.interact.ts",
|
|
285
|
+
capabilities: ["terminal-list-contacts", "terminal-create-contact"],
|
|
286
|
+
},
|
|
287
|
+
"plugins/plugin-hyperliquid-app": {
|
|
288
|
+
srcFile:
|
|
289
|
+
"plugins/plugin-hyperliquid-app/src/HyperliquidAppView.interact.ts",
|
|
290
|
+
capabilities: ["terminal-hyperliquid-state"],
|
|
291
|
+
},
|
|
292
|
+
"plugins/plugin-messages": {
|
|
293
|
+
srcFile:
|
|
294
|
+
"plugins/plugin-messages/src/components/MessagesAppView.interact.ts",
|
|
295
|
+
capabilities: ["terminal-list-threads", "terminal-send-sms"],
|
|
296
|
+
},
|
|
297
|
+
"plugins/plugin-phone": {
|
|
298
|
+
srcFile: "plugins/plugin-phone/src/components/PhoneAppView.interact.ts",
|
|
299
|
+
capabilities: ["terminal-phone-state", "terminal-place-call"],
|
|
300
|
+
},
|
|
301
|
+
"plugins/plugin-wallet-ui": {
|
|
302
|
+
srcFile: "plugins/plugin-wallet-ui/src/InventoryView.interact.ts",
|
|
303
|
+
capabilities: ["terminal-wallet-state"],
|
|
304
|
+
},
|
|
305
|
+
"plugins/plugin-feed": {
|
|
306
|
+
srcFile: "plugins/plugin-feed/src/ui/FeedOperatorSurface.interact.ts",
|
|
307
|
+
capabilities: ["get-state", "refresh-agent-status"],
|
|
308
|
+
},
|
|
309
|
+
"plugins/plugin-screenshare": {
|
|
310
|
+
srcFile:
|
|
311
|
+
"plugins/plugin-screenshare/src/ui/ScreenshareOperatorSurface.interact.ts",
|
|
312
|
+
capabilities: ["terminal-screenshare-state", "terminal-screenshare-start"],
|
|
313
|
+
},
|
|
314
|
+
"plugins/plugin-task-coordinator": {
|
|
315
|
+
srcFile:
|
|
316
|
+
"plugins/plugin-task-coordinator/src/CodingAgentTasksPanel.interact.ts",
|
|
317
|
+
capabilities: ["list-sessions", "list-task-threads"],
|
|
318
|
+
},
|
|
319
|
+
"plugins/plugin-trajectory-logger": {
|
|
320
|
+
srcFile:
|
|
321
|
+
"plugins/plugin-trajectory-logger/src/components/TrajectoryLoggerView.interact.ts",
|
|
322
|
+
capabilities: ["list-trajectories", "open-latest"],
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
describe("XR feature-by-feature functional parity — all 18 views", () => {
|
|
329
|
+
// A. Shared bundle architecture ─────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
it("A — every XR view uses the same bundlePath as the GUI view (shared bundle = shared features)", () => {
|
|
332
|
+
const failures: string[] = [];
|
|
333
|
+
for (const { pluginDir, manifestPath } of PLUGIN_REGISTRY) {
|
|
334
|
+
const source = readFile(manifestPath);
|
|
335
|
+
const entries = parseViewEntries(source);
|
|
336
|
+
const guiEntry = entries.find((e) => e.viewType === "gui");
|
|
337
|
+
const xrEntry = entries.find((e) => e.viewType === "xr");
|
|
338
|
+
if (!guiEntry) {
|
|
339
|
+
failures.push(`${pluginDir}: no gui view found`);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (!xrEntry) {
|
|
343
|
+
failures.push(`${pluginDir}: no xr view found`);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (guiEntry.bundlePath !== xrEntry.bundlePath) {
|
|
347
|
+
failures.push(
|
|
348
|
+
`${pluginDir}: gui bundlePath="${guiEntry.bundlePath}" ≠ xr bundlePath="${xrEntry.bundlePath}"`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
expect(
|
|
353
|
+
failures,
|
|
354
|
+
"plugins where XR uses a different bundle than GUI",
|
|
355
|
+
).toEqual([]);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("A — every XR view exports the same React component as the GUI view", () => {
|
|
359
|
+
const failures: string[] = [];
|
|
360
|
+
for (const { pluginDir, manifestPath } of PLUGIN_REGISTRY) {
|
|
361
|
+
const source = readFile(manifestPath);
|
|
362
|
+
const entries = parseViewEntries(source);
|
|
363
|
+
const guiEntry = entries.find((e) => e.viewType === "gui");
|
|
364
|
+
const xrEntry = entries.find((e) => e.viewType === "xr");
|
|
365
|
+
if (!guiEntry || !xrEntry) continue;
|
|
366
|
+
// Normalize: strip package#Name prefix if present
|
|
367
|
+
const normalize = (s: string) =>
|
|
368
|
+
s.includes("#") ? (s.split("#").pop() ?? s) : s;
|
|
369
|
+
if (
|
|
370
|
+
normalize(guiEntry.componentExport) !==
|
|
371
|
+
normalize(xrEntry.componentExport)
|
|
372
|
+
) {
|
|
373
|
+
failures.push(
|
|
374
|
+
`${pluginDir}: gui exports "${guiEntry.componentExport}" but xr exports "${xrEntry.componentExport}"`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
expect(
|
|
379
|
+
failures,
|
|
380
|
+
"plugins where XR uses a different component than GUI",
|
|
381
|
+
).toEqual([]);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// B. Component source functional content ────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
it("B — each XR view's component source file exists and is non-empty TSX", () => {
|
|
387
|
+
const failures: string[] = [];
|
|
388
|
+
for (const { pluginDir, xrComponentSrc } of PLUGIN_REGISTRY) {
|
|
389
|
+
if (!fileExists(xrComponentSrc)) {
|
|
390
|
+
failures.push(`${pluginDir}: ${xrComponentSrc} does not exist`);
|
|
391
|
+
} else {
|
|
392
|
+
const src = readFile(xrComponentSrc);
|
|
393
|
+
if (src.length < 100) {
|
|
394
|
+
failures.push(
|
|
395
|
+
`${pluginDir}: ${xrComponentSrc} is too short (${src.length} bytes)`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
expect(failures, "missing or empty XR component source files").toEqual([]);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("B — each XR component source contains its required functional UI terms", () => {
|
|
404
|
+
const failures: string[] = [];
|
|
405
|
+
for (const {
|
|
406
|
+
pluginDir,
|
|
407
|
+
xrComponentSrc,
|
|
408
|
+
requiredTerms,
|
|
409
|
+
} of PLUGIN_REGISTRY) {
|
|
410
|
+
if (!fileExists(xrComponentSrc)) continue;
|
|
411
|
+
const src = readComponentFamily(xrComponentSrc);
|
|
412
|
+
for (const term of requiredTerms) {
|
|
413
|
+
if (!src.includes(term)) {
|
|
414
|
+
failures.push(
|
|
415
|
+
`${pluginDir}: "${term}" not found in ${xrComponentSrc}`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
expect(failures, "components missing required functional content").toEqual(
|
|
421
|
+
[],
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("B — all 18 XR component sources use React hooks (useState/useEffect) for stateful UIs", () => {
|
|
426
|
+
const noHooks: string[] = [];
|
|
427
|
+
for (const { pluginDir, xrComponentSrc } of PLUGIN_REGISTRY) {
|
|
428
|
+
if (!fileExists(xrComponentSrc)) continue;
|
|
429
|
+
const src = readComponentFamily(xrComponentSrc);
|
|
430
|
+
if (
|
|
431
|
+
!src.includes("useState") &&
|
|
432
|
+
!src.includes("useEffect") &&
|
|
433
|
+
!src.includes("useRef") &&
|
|
434
|
+
!src.includes("useCallback") &&
|
|
435
|
+
!src.includes("useRenderGuard")
|
|
436
|
+
) {
|
|
437
|
+
noHooks.push(`${pluginDir}: ${xrComponentSrc}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
expect(
|
|
441
|
+
noHooks,
|
|
442
|
+
"XR components with no React hooks (likely static shells)",
|
|
443
|
+
).toEqual([]);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// C. Bundle exports the declared component symbol ───────────────────────────
|
|
447
|
+
|
|
448
|
+
it("C — built bundle.js files export their declared componentExport symbols", () => {
|
|
449
|
+
const failures: string[] = [];
|
|
450
|
+
for (const { pluginDir, manifestPath } of PLUGIN_REGISTRY) {
|
|
451
|
+
const bundlePath = `${pluginDir}/dist/views/bundle.js`;
|
|
452
|
+
if (!fileExists(bundlePath)) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const bundle = readFile(bundlePath);
|
|
456
|
+
const source = readFile(manifestPath);
|
|
457
|
+
const xrEntry = parseViewEntries(source).find((e) => e.viewType === "xr");
|
|
458
|
+
if (!xrEntry) {
|
|
459
|
+
failures.push(`${pluginDir}: no xr entry`);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const exportName = xrEntry.componentExport.includes("#")
|
|
463
|
+
? (xrEntry.componentExport.split("#").pop() ?? xrEntry.componentExport)
|
|
464
|
+
: xrEntry.componentExport;
|
|
465
|
+
if (!bundle.includes(exportName)) {
|
|
466
|
+
failures.push(`${pluginDir}: bundle does not contain "${exportName}"`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
expect(failures, "bundles missing declared XR component").toEqual([]);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// D. Agent-facing TUI capabilities in shared source ─────────────────────────
|
|
473
|
+
|
|
474
|
+
it("D — agent TUI capabilities are present in the shared XR component source (GUI=XR=TUI via same component)", () => {
|
|
475
|
+
const failures: string[] = [];
|
|
476
|
+
for (const [pluginDir, { srcFile, capabilities }] of Object.entries(
|
|
477
|
+
TUI_CAPABILITY_SOURCE_MAP,
|
|
478
|
+
)) {
|
|
479
|
+
if (!fileExists(srcFile)) {
|
|
480
|
+
failures.push(`${pluginDir}: source file ${srcFile} missing`);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const src = readComponentFamily(srcFile);
|
|
484
|
+
for (const cap of capabilities) {
|
|
485
|
+
if (!src.includes(cap)) {
|
|
486
|
+
failures.push(`${pluginDir}: capability "${cap}" not in ${srcFile}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
expect(
|
|
491
|
+
failures,
|
|
492
|
+
"TUI capabilities missing from shared XR+GUI component source",
|
|
493
|
+
).toEqual([]);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Summary assertion ─────────────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
it("summary — all 18 plugins have XR views that are functionally identical to their GUI views", () => {
|
|
499
|
+
// This test is a logical consequence of tests A, B, C, D above all passing.
|
|
500
|
+
// It explicitly states the guarantee: same bundle + same component = same features.
|
|
501
|
+
const xrPluginCount = PLUGIN_REGISTRY.length;
|
|
502
|
+
expect(xrPluginCount).toBe(18);
|
|
503
|
+
|
|
504
|
+
for (const { pluginDir, manifestPath } of PLUGIN_REGISTRY) {
|
|
505
|
+
const source = readFile(manifestPath);
|
|
506
|
+
const entries = parseViewEntries(source);
|
|
507
|
+
const guiEntry = entries.find((e) => e.viewType === "gui");
|
|
508
|
+
const xrEntry = entries.find((e) => e.viewType === "xr");
|
|
509
|
+
|
|
510
|
+
expect(guiEntry, `${pluginDir}: GUI view must exist`).toBeDefined();
|
|
511
|
+
expect(xrEntry, `${pluginDir}: XR view must exist`).toBeDefined();
|
|
512
|
+
|
|
513
|
+
if (guiEntry && xrEntry) {
|
|
514
|
+
// The architectural guarantee: XR is GUI is the same component
|
|
515
|
+
expect(
|
|
516
|
+
guiEntry.bundlePath,
|
|
517
|
+
`${pluginDir}: XR bundle must equal GUI bundle`,
|
|
518
|
+
).toBe(xrEntry.bundlePath);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
});
|