@elizaos/plugin-xr 2.0.3-beta.5 → 2.0.3-beta.7
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/dist/actions/xr-query-vision.d.ts +3 -0
- package/dist/actions/xr-query-vision.d.ts.map +1 -0
- package/dist/actions/xr-query-vision.js +39 -0
- package/dist/actions/xr-query-vision.js.map +1 -0
- package/dist/actions/xr-view-actions.d.ts +18 -0
- package/dist/actions/xr-view-actions.d.ts.map +1 -0
- package/dist/actions/xr-view-actions.js +304 -0
- package/dist/actions/xr-view-actions.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +124 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +18 -0
- package/dist/protocol.js.map +1 -0
- package/dist/providers/xr-context.d.ts +3 -0
- package/dist/providers/xr-context.d.ts.map +1 -0
- package/dist/providers/xr-context.js +34 -0
- package/dist/providers/xr-context.js.map +1 -0
- package/dist/routes/xr-connect.d.ts +3 -0
- package/dist/routes/xr-connect.d.ts.map +1 -0
- package/{src/routes/xr-connect.ts → dist/routes/xr-connect.js} +12 -15
- package/dist/routes/xr-connect.js.map +1 -0
- package/dist/routes/xr-simulator-route.d.ts +8 -0
- package/dist/routes/xr-simulator-route.d.ts.map +1 -0
- package/{src/routes/xr-simulator-route.ts → dist/routes/xr-simulator-route.js} +10 -16
- package/dist/routes/xr-simulator-route.js.map +1 -0
- package/dist/routes/xr-status.d.ts +3 -0
- package/dist/routes/xr-status.d.ts.map +1 -0
- package/{src/routes/xr-status.ts → dist/routes/xr-status.js} +13 -15
- package/dist/routes/xr-status.js.map +1 -0
- package/dist/routes/xr-view-host.d.ts +24 -0
- package/dist/routes/xr-view-host.d.ts.map +1 -0
- package/{src/routes/xr-view-host.ts → dist/routes/xr-view-host.js} +22 -59
- package/dist/routes/xr-view-host.js.map +1 -0
- package/dist/routes/xr-views.d.ts +8 -0
- package/dist/routes/xr-views.d.ts.map +1 -0
- package/dist/routes/xr-views.js +31 -0
- package/dist/routes/xr-views.js.map +1 -0
- package/dist/services/audio-pipeline.d.ts +20 -0
- package/dist/services/audio-pipeline.d.ts.map +1 -0
- package/{src/services/audio-pipeline.ts → dist/services/audio-pipeline.js} +25 -58
- package/dist/services/audio-pipeline.js.map +1 -0
- package/dist/services/vision-pipeline.d.ts +16 -0
- package/dist/services/vision-pipeline.d.ts.map +1 -0
- package/dist/services/vision-pipeline.js +39 -0
- package/dist/services/vision-pipeline.js.map +1 -0
- package/dist/services/xr-session-service.d.ts +50 -0
- package/dist/services/xr-session-service.d.ts.map +1 -0
- package/{src/services/xr-session-service.ts → dist/services/xr-session-service.js} +85 -194
- package/dist/services/xr-session-service.js.map +1 -0
- package/package.json +9 -4
- package/AGENTS.md +0 -151
- package/CLAUDE.md +0 -151
- package/simulator/bun.lock +0 -159
- package/simulator/package.json +0 -28
- package/simulator/src/emulator.ts +0 -174
- package/simulator/src/mock-agent.ts +0 -233
- package/simulator/src/node.ts +0 -9
- package/simulator/src/playwright-fixture.ts +0 -169
- package/simulator/src/types.ts +0 -51
- package/simulator/tsconfig.json +0 -13
- package/simulator/vite.config.ts +0 -25
- package/src/__tests__/audio-pipeline.test.ts +0 -129
- package/src/__tests__/protocol.test.ts +0 -53
- package/src/__tests__/routes-e2e.test.ts +0 -276
- package/src/__tests__/vision-pipeline.test.ts +0 -73
- package/src/__tests__/xr-bundle-coverage.test.ts +0 -303
- package/src/__tests__/xr-feature-parity.test.ts +0 -524
- package/src/__tests__/xr-functional-parity.test.ts +0 -522
- package/src/__tests__/xr-view-host-http.test.ts +0 -239
- package/src/__tests__/xr-view-host.test.ts +0 -174
- package/src/actions/xr-query-vision.ts +0 -64
- package/src/actions/xr-view-actions.ts +0 -386
- package/src/index.ts +0 -55
- package/src/protocol.ts +0 -126
- package/src/providers/xr-context.ts +0 -49
- package/src/routes/xr-views.ts +0 -43
- package/src/services/vision-pipeline.ts +0 -57
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -30
- package/vitest.config.ts +0 -21
|
@@ -1,524 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* XR feature parity audit — automated.
|
|
3
|
-
*
|
|
4
|
-
* This test suite formally validates the claim that the XR app (app-xr)
|
|
5
|
-
* provides 100% feature parity with the native iOS / Android / desktop app
|
|
6
|
-
* for every capability that can be expressed through the agent view system.
|
|
7
|
-
*
|
|
8
|
-
* Parity axes:
|
|
9
|
-
* 1. View registration — every gui view has a matching xr view
|
|
10
|
-
* 2. Route infrastructure — every xr view id has a working view-host route
|
|
11
|
-
* 3. Agent CRUD surface — all 5 agent actions are wired in plugin-xr
|
|
12
|
-
* 4. Connection modes — Local/Cloud/Custom all represented in code
|
|
13
|
-
* 5. Voice input — transcript routing is wired in view-host for all views
|
|
14
|
-
* 6. Platform manifest — both APK configurations are present
|
|
15
|
-
* 7. PWA manifest — app-xr has a complete web manifest
|
|
16
|
-
* 8. HTTPS tunnel — connect script produces a shareable URL
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
-
import { dirname, resolve } from "node:path";
|
|
21
|
-
import { fileURLToPath } from "node:url";
|
|
22
|
-
import {
|
|
23
|
-
registerPluginViews,
|
|
24
|
-
unregisterPluginViews,
|
|
25
|
-
} from "@elizaos/agent/api/views-registry";
|
|
26
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
27
|
-
import {
|
|
28
|
-
xrListViewsAction,
|
|
29
|
-
xrOpenViewAction,
|
|
30
|
-
} from "../actions/xr-view-actions.ts";
|
|
31
|
-
import { xrViewHostRoute } from "../routes/xr-view-host.ts";
|
|
32
|
-
import { xrViewsRoute } from "../routes/xr-views.ts";
|
|
33
|
-
|
|
34
|
-
const repoRoot = resolve(
|
|
35
|
-
dirname(fileURLToPath(import.meta.url)),
|
|
36
|
-
"../../../..",
|
|
37
|
-
);
|
|
38
|
-
const appXrRoot = resolve(repoRoot, "plugins/plugin-facewear/app-xr");
|
|
39
|
-
const facewearAndroidRoot = resolve(
|
|
40
|
-
repoRoot,
|
|
41
|
-
"plugins/plugin-facewear/native/android",
|
|
42
|
-
);
|
|
43
|
-
const XR_ROUTE_TEST_PLUGIN = "@test/plugin-xr-route-registry";
|
|
44
|
-
|
|
45
|
-
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
function readFile(relPath: string): string {
|
|
48
|
-
return readFileSync(resolve(repoRoot, relPath), "utf8");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function appXrFileExists(relPath: string): boolean {
|
|
52
|
-
return existsSync(resolve(appXrRoot, relPath));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function readAppXr(relPath: string): string {
|
|
56
|
-
return readFileSync(resolve(appXrRoot, relPath), "utf8");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function facewearAndroidFileExists(relPath: string): boolean {
|
|
60
|
-
return existsSync(resolve(facewearAndroidRoot, relPath));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function readFacewearAndroid(relPath: string): string {
|
|
64
|
-
return readFileSync(resolve(facewearAndroidRoot, relPath), "utf8");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function hasAppXr(): boolean {
|
|
68
|
-
return appXrFileExists("package.json");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Parses `views: [...]` from a plugin source file
|
|
72
|
-
function extractViewObjects(source: string): string[] {
|
|
73
|
-
const viewsStart = source.indexOf("views:");
|
|
74
|
-
if (viewsStart === -1) return [];
|
|
75
|
-
const arrayStart = source.indexOf("[", viewsStart);
|
|
76
|
-
if (arrayStart === -1) return [];
|
|
77
|
-
let depth = 0;
|
|
78
|
-
let arrayEnd = -1;
|
|
79
|
-
for (let i = arrayStart; i < source.length; i++) {
|
|
80
|
-
if (source[i] === "[") depth++;
|
|
81
|
-
if (source[i] === "]") depth--;
|
|
82
|
-
if (depth === 0) {
|
|
83
|
-
arrayEnd = i;
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (arrayEnd === -1) return [];
|
|
88
|
-
const body = source.slice(arrayStart + 1, arrayEnd);
|
|
89
|
-
const objects: string[] = [];
|
|
90
|
-
let start = -1;
|
|
91
|
-
depth = 0;
|
|
92
|
-
for (let i = 0; i < body.length; i++) {
|
|
93
|
-
if (body[i] === "{") {
|
|
94
|
-
if (depth === 0) start = i;
|
|
95
|
-
depth++;
|
|
96
|
-
}
|
|
97
|
-
if (body[i] === "}") {
|
|
98
|
-
depth--;
|
|
99
|
-
if (depth === 0 && start !== -1) {
|
|
100
|
-
objects.push(body.slice(start, i + 1));
|
|
101
|
-
start = -1;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return objects.filter(
|
|
106
|
-
(o) => o.includes("id:") && o.includes("componentExport:"),
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function stringField(source: string, field: string): string | null {
|
|
111
|
-
return source.match(new RegExp(`${field}:\\s*"([^"]+)"`))?.[1] ?? null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Source-level mirror of core's `getViewModalities`: a view renders on the
|
|
115
|
-
// explicit `modalities: [...]` list when present, otherwise the single
|
|
116
|
-
// `viewType` (default "gui"). Returns the lowercased modality set for one
|
|
117
|
-
// view-object source slice.
|
|
118
|
-
function viewModalities(objectSource: string): Set<string> {
|
|
119
|
-
const modalitiesMatch = objectSource.match(/modalities:\s*\[([^\]]*)\]/);
|
|
120
|
-
if (modalitiesMatch) {
|
|
121
|
-
const mods = [...modalitiesMatch[1].matchAll(/"([^"]+)"/g)].map(
|
|
122
|
-
(m) => m[1],
|
|
123
|
-
);
|
|
124
|
-
if (mods.length > 0) return new Set(mods);
|
|
125
|
-
}
|
|
126
|
-
return new Set([stringField(objectSource, "viewType") ?? "gui"]);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const VIEW_HOST_SMOKE_IDS = [
|
|
130
|
-
"xr-route-smoke",
|
|
131
|
-
"hyphenated-view",
|
|
132
|
-
"space-panel",
|
|
133
|
-
] as const;
|
|
134
|
-
|
|
135
|
-
const VOICE_ROUTE_SAMPLE_IDS = [
|
|
136
|
-
"xr-route-smoke",
|
|
137
|
-
"hyphenated-view",
|
|
138
|
-
"space-panel",
|
|
139
|
-
] as const;
|
|
140
|
-
|
|
141
|
-
// The plugin manifest paths (same as plugin-tui-view-coverage.test.ts)
|
|
142
|
-
const VIEW_MANIFESTS = [
|
|
143
|
-
"plugins/plugin-companion/src/plugin.ts",
|
|
144
|
-
"plugins/plugin-contacts/src/plugin.ts",
|
|
145
|
-
"plugins/plugin-hyperliquid-app/src/plugin.ts",
|
|
146
|
-
"plugins/plugin-messages/src/plugin.ts",
|
|
147
|
-
"plugins/app-model-tester/src/plugin.ts",
|
|
148
|
-
"plugins/plugin-phone/src/plugin.ts",
|
|
149
|
-
"plugins/plugin-polymarket-app/src/plugin.ts",
|
|
150
|
-
"plugins/plugin-shopify-ui/src/plugin.ts",
|
|
151
|
-
"plugins/plugin-steward-app/src/plugin.ts",
|
|
152
|
-
"plugins/plugin-vincent/src/plugin.ts",
|
|
153
|
-
"plugins/plugin-wallet-ui/src/plugin.ts",
|
|
154
|
-
"plugins/plugin-feed/src/index.ts",
|
|
155
|
-
"plugins/plugin-app-control/src/index.ts",
|
|
156
|
-
"plugins/plugin-screenshare/src/index.ts",
|
|
157
|
-
"plugins/plugin-task-coordinator/src/index.ts",
|
|
158
|
-
"plugins/plugin-trajectory-logger/src/index.ts",
|
|
159
|
-
"plugins/plugin-training/src/setup-routes.ts",
|
|
160
|
-
"plugins/plugin-facewear/src/index.ts",
|
|
161
|
-
] as const;
|
|
162
|
-
|
|
163
|
-
// ── tests ─────────────────────────────────────────────────────────────────────
|
|
164
|
-
|
|
165
|
-
describe("XR feature parity audit", () => {
|
|
166
|
-
afterEach(() => {
|
|
167
|
-
unregisterPluginViews(XR_ROUTE_TEST_PLUGIN);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// 1. View registration parity ───────────────────────────────────────────────
|
|
171
|
-
|
|
172
|
-
it("axis 1 — every gui plugin view has a matching xr view in the plugin manifest", () => {
|
|
173
|
-
const missing: string[] = [];
|
|
174
|
-
for (const manifestPath of VIEW_MANIFESTS) {
|
|
175
|
-
const source = readFile(manifestPath);
|
|
176
|
-
const objects = extractViewObjects(source);
|
|
177
|
-
const guiIds = new Set<string>();
|
|
178
|
-
const xrIds = new Set<string>();
|
|
179
|
-
for (const obj of objects) {
|
|
180
|
-
const id = stringField(obj, "id");
|
|
181
|
-
if (!id) continue;
|
|
182
|
-
const modalities = viewModalities(obj);
|
|
183
|
-
if (modalities.has("xr")) xrIds.add(id);
|
|
184
|
-
if (modalities.has("gui")) guiIds.add(id);
|
|
185
|
-
}
|
|
186
|
-
for (const id of guiIds) {
|
|
187
|
-
if (!xrIds.has(id))
|
|
188
|
-
missing.push(`${manifestPath}: missing xr view for "${id}"`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
expect(missing, "plugins missing XR views").toEqual([]);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// 2. Route infrastructure ───────────────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
it("axis 2 — the xrViewHostRoute returns valid HTML for arbitrary xr view ids", async () => {
|
|
197
|
-
const failures: string[] = [];
|
|
198
|
-
for (const id of VIEW_HOST_SMOKE_IDS) {
|
|
199
|
-
const result = await xrViewHostRoute.routeHandler({
|
|
200
|
-
params: { id },
|
|
201
|
-
runtime: { port: 31337 },
|
|
202
|
-
} as never);
|
|
203
|
-
if (result.status !== 200) {
|
|
204
|
-
failures.push(`${id}: status ${result.status}`);
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
const html = result.body as string;
|
|
208
|
-
if (!html.includes(`data-view-id="${id}"`))
|
|
209
|
-
failures.push(`${id}: data-view-id not in HTML`);
|
|
210
|
-
if (!html.includes('id="xr-shell"'))
|
|
211
|
-
failures.push(`${id}: missing xr-shell`);
|
|
212
|
-
}
|
|
213
|
-
expect(failures, "view-host route failures").toEqual([]);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("axis 2 — xrViewsRoute source is registered as GET /xr/views through the canonical registry", () => {
|
|
217
|
-
const routeSrc = readFile("plugins/plugin-xr/src/routes/xr-views.ts");
|
|
218
|
-
expect(routeSrc).toContain('"GET"');
|
|
219
|
-
expect(routeSrc).toContain('"/xr/views"');
|
|
220
|
-
expect(routeSrc).toContain("@elizaos/agent/api/views-registry");
|
|
221
|
-
expect(routeSrc).toContain('viewType: "xr"');
|
|
222
|
-
// Returns view list with count
|
|
223
|
-
expect(routeSrc).toContain("count");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("axis 2 — xrViewsRoute returns canonical registry XR entries", async () => {
|
|
227
|
-
await registerPluginViews({
|
|
228
|
-
name: XR_ROUTE_TEST_PLUGIN,
|
|
229
|
-
views: [
|
|
230
|
-
{
|
|
231
|
-
id: "xr-registry-route-smoke",
|
|
232
|
-
label: "XR Registry Route",
|
|
233
|
-
viewType: "xr",
|
|
234
|
-
path: "/apps/xr-registry-route-smoke/xr",
|
|
235
|
-
icon: "Glasses",
|
|
236
|
-
tags: ["xr", "registry"],
|
|
237
|
-
description: "Registry-backed XR route smoke",
|
|
238
|
-
xrOptions: { placement: "panel" },
|
|
239
|
-
bundleUrl: "https://views.example.test/xr-registry-route.js",
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
id: "gui-registry-route-smoke",
|
|
243
|
-
label: "GUI Registry Route",
|
|
244
|
-
viewType: "gui",
|
|
245
|
-
path: "/apps/gui-registry-route-smoke",
|
|
246
|
-
bundleUrl: "https://views.example.test/gui-registry-route.js",
|
|
247
|
-
},
|
|
248
|
-
],
|
|
249
|
-
} as never);
|
|
250
|
-
|
|
251
|
-
const result = await xrViewsRoute.routeHandler({
|
|
252
|
-
runtime: {
|
|
253
|
-
getService: () => ({
|
|
254
|
-
getConnections: () => [{ id: "headset-1", deviceType: "webxr" }],
|
|
255
|
-
}),
|
|
256
|
-
},
|
|
257
|
-
} as never);
|
|
258
|
-
|
|
259
|
-
expect(result.status).toBe(200);
|
|
260
|
-
expect(result.body).toMatchObject({
|
|
261
|
-
count: expect.any(Number),
|
|
262
|
-
connections: [{ id: "headset-1", deviceType: "webxr" }],
|
|
263
|
-
});
|
|
264
|
-
expect(
|
|
265
|
-
(result.body as { views: Array<Record<string, unknown>> }).views,
|
|
266
|
-
).toContainEqual(
|
|
267
|
-
expect.objectContaining({
|
|
268
|
-
id: "xr-registry-route-smoke",
|
|
269
|
-
label: "XR Registry Route",
|
|
270
|
-
path: "/apps/xr-registry-route-smoke/xr",
|
|
271
|
-
pluginName: XR_ROUTE_TEST_PLUGIN,
|
|
272
|
-
available: true,
|
|
273
|
-
xrOptions: { placement: "panel" },
|
|
274
|
-
}),
|
|
275
|
-
);
|
|
276
|
-
expect(
|
|
277
|
-
(result.body as { views: Array<Record<string, unknown>> }).views.some(
|
|
278
|
-
(view) => view.id === "gui-registry-route-smoke",
|
|
279
|
-
),
|
|
280
|
-
).toBe(false);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// 3. Agent CRUD action surface ──────────────────────────────────────────────
|
|
284
|
-
|
|
285
|
-
it("axis 3 — plugin-xr exports all 5 agent view actions", () => {
|
|
286
|
-
const actionsSource = readFile(
|
|
287
|
-
"plugins/plugin-xr/src/actions/xr-view-actions.ts",
|
|
288
|
-
);
|
|
289
|
-
const requiredActions = [
|
|
290
|
-
"XR_OPEN_VIEW",
|
|
291
|
-
"XR_CLOSE_VIEW",
|
|
292
|
-
"XR_SWITCH_VIEW",
|
|
293
|
-
"XR_LIST_VIEWS",
|
|
294
|
-
"XR_RESIZE_VIEW",
|
|
295
|
-
];
|
|
296
|
-
const missing = requiredActions.filter((a) => !actionsSource.includes(a));
|
|
297
|
-
expect(missing, "missing agent actions").toEqual([]);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it("axis 3 — view actions route dynamically registered XR views", async () => {
|
|
301
|
-
await registerPluginViews({
|
|
302
|
-
name: XR_ROUTE_TEST_PLUGIN,
|
|
303
|
-
views: [
|
|
304
|
-
{
|
|
305
|
-
id: "xr-dynamic-action-panel",
|
|
306
|
-
label: "Dynamic Action Panel",
|
|
307
|
-
viewType: "xr",
|
|
308
|
-
path: "/apps/xr-dynamic-action-panel/xr",
|
|
309
|
-
icon: "Glasses",
|
|
310
|
-
description: "Dynamically registered action target",
|
|
311
|
-
bundleUrl: "https://views.example.test/xr-dynamic-action-panel.js",
|
|
312
|
-
},
|
|
313
|
-
],
|
|
314
|
-
} as never);
|
|
315
|
-
|
|
316
|
-
const calls = {
|
|
317
|
-
opened: [] as Array<{ connectionId: string; viewId: string }>,
|
|
318
|
-
catalogs: [] as Array<Array<{ id: string; label: string }>>,
|
|
319
|
-
};
|
|
320
|
-
const runtime = {
|
|
321
|
-
port: 31337,
|
|
322
|
-
getService: () => ({
|
|
323
|
-
getConnections: () => [{ id: "headset-1", deviceType: "webxr" }],
|
|
324
|
-
hasActiveConnections: () => true,
|
|
325
|
-
openView: (connectionId: string, viewId: string) => {
|
|
326
|
-
calls.opened.push({ connectionId, viewId });
|
|
327
|
-
},
|
|
328
|
-
sendViewsCatalog: (
|
|
329
|
-
_connectionId: string,
|
|
330
|
-
views: Array<{ id: string; label: string }>,
|
|
331
|
-
) => {
|
|
332
|
-
calls.catalogs.push(views);
|
|
333
|
-
},
|
|
334
|
-
}),
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
await xrOpenViewAction.handler?.(
|
|
338
|
-
runtime as never,
|
|
339
|
-
{ content: { text: "open the dynamic action panel in xr" } } as never,
|
|
340
|
-
undefined,
|
|
341
|
-
{},
|
|
342
|
-
);
|
|
343
|
-
await xrListViewsAction.handler?.(
|
|
344
|
-
runtime as never,
|
|
345
|
-
{ content: { text: "what can i open in xr?" } } as never,
|
|
346
|
-
undefined,
|
|
347
|
-
{},
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
expect(calls.opened).toContainEqual({
|
|
351
|
-
connectionId: "headset-1",
|
|
352
|
-
viewId: "xr-dynamic-action-panel",
|
|
353
|
-
});
|
|
354
|
-
expect(calls.catalogs.at(-1)).toContainEqual(
|
|
355
|
-
expect.objectContaining({
|
|
356
|
-
id: "xr-dynamic-action-panel",
|
|
357
|
-
label: "Dynamic Action Panel",
|
|
358
|
-
}),
|
|
359
|
-
);
|
|
360
|
-
const actionsSource = readFile(
|
|
361
|
-
"plugins/plugin-xr/src/actions/xr-view-actions.ts",
|
|
362
|
-
);
|
|
363
|
-
expect(actionsSource).toContain("@elizaos/agent/api/views-registry");
|
|
364
|
-
expect(actionsSource).not.toContain("const known = [");
|
|
365
|
-
expect(actionsSource).not.toContain("runtime.plugins");
|
|
366
|
-
expect(actionsSource).not.toContain("RuntimePluginWithViews");
|
|
367
|
-
expect(actionsSource).not.toContain("plugin.views");
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// 4. Connection modes ───────────────────────────────────────────────────────
|
|
371
|
-
|
|
372
|
-
it("axis 4 — app-xr connection-config.ts implements Local/Cloud/Custom modes", () => {
|
|
373
|
-
if (!hasAppXr()) return;
|
|
374
|
-
const src = readAppXr("src/connection-config.ts");
|
|
375
|
-
expect(src).toContain('"local"');
|
|
376
|
-
expect(src).toContain('"cloud"');
|
|
377
|
-
expect(src).toContain('"custom"');
|
|
378
|
-
expect(src).toContain("configToWsUrl");
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it("axis 4 — app-xr connection-setup.ts renders the mode picker UI", () => {
|
|
382
|
-
if (!hasAppXr()) return;
|
|
383
|
-
const src = readAppXr("src/ui/connection-setup.ts");
|
|
384
|
-
expect(src).toContain("local");
|
|
385
|
-
expect(src).toContain("cloud");
|
|
386
|
-
expect(src).toContain("custom");
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it("axis 4 — AgentSocket supports hot reconnect for mode switching", () => {
|
|
390
|
-
if (!hasAppXr()) return;
|
|
391
|
-
const socketSrc = readAppXr("src/agent-socket.ts");
|
|
392
|
-
expect(socketSrc).toContain("reconnectTo");
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
// 5. Voice input ────────────────────────────────────────────────────────────
|
|
396
|
-
|
|
397
|
-
it("axis 5 — view-host pages have voice transcript routing for INPUT, TEXTAREA, SELECT, and ARIA widgets", async () => {
|
|
398
|
-
for (const id of VOICE_ROUTE_SAMPLE_IDS) {
|
|
399
|
-
const result = await xrViewHostRoute.routeHandler({
|
|
400
|
-
params: { id },
|
|
401
|
-
runtime: { port: 31337 },
|
|
402
|
-
} as never);
|
|
403
|
-
const html = result.body as string;
|
|
404
|
-
expect(html, `${id}: fillFocusedInput for INPUT`).toContain(
|
|
405
|
-
"HTMLInputElement",
|
|
406
|
-
);
|
|
407
|
-
expect(html, `${id}: fillFocusedInput for TEXTAREA`).toContain(
|
|
408
|
-
"HTMLTextAreaElement",
|
|
409
|
-
);
|
|
410
|
-
expect(html, `${id}: fillFocusedInput for SELECT`).toContain(
|
|
411
|
-
"HTMLSelectElement",
|
|
412
|
-
);
|
|
413
|
-
expect(html, `${id}: ARIA combobox/listbox routing`).toContain(
|
|
414
|
-
"combobox",
|
|
415
|
-
);
|
|
416
|
-
expect(html, `${id}: xr:focus-next handler`).toContain("focus-next");
|
|
417
|
-
expect(html, `${id}: voice indicator`).toContain("voice-indicator");
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
// 6. Platform APK manifests ─────────────────────────────────────────────────
|
|
422
|
-
|
|
423
|
-
it("axis 6 — Quest 3 Bubblewrap APK configuration is present and complete", () => {
|
|
424
|
-
if (!hasAppXr()) return;
|
|
425
|
-
expect(facewearAndroidFileExists("quest/bubblewrap.json")).toBe(true);
|
|
426
|
-
const config = JSON.parse(readFacewearAndroid("quest/bubblewrap.json"));
|
|
427
|
-
expect(config.packageId).toBe("com.eliza.xr.quest");
|
|
428
|
-
expect(config.metaQuest).toBe(true);
|
|
429
|
-
expect(config.permissions).toContain("android.permission.CAMERA");
|
|
430
|
-
expect(config.permissions).toContain("android.permission.RECORD_AUDIO");
|
|
431
|
-
expect(config.display).toBe("fullscreen");
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
it("axis 6 — XReal Android project has complete Gradle project structure", () => {
|
|
435
|
-
if (!hasAppXr()) return;
|
|
436
|
-
expect(facewearAndroidFileExists("xreal/build.gradle.kts")).toBe(true);
|
|
437
|
-
expect(facewearAndroidFileExists("xreal/settings.gradle.kts")).toBe(true);
|
|
438
|
-
expect(facewearAndroidFileExists("xreal/gradlew")).toBe(true);
|
|
439
|
-
expect(
|
|
440
|
-
facewearAndroidFileExists(
|
|
441
|
-
"xreal/gradle/wrapper/gradle-wrapper.properties",
|
|
442
|
-
),
|
|
443
|
-
).toBe(true);
|
|
444
|
-
expect(facewearAndroidFileExists("xreal/app/build.gradle.kts")).toBe(true);
|
|
445
|
-
expect(
|
|
446
|
-
facewearAndroidFileExists("xreal/app/src/main/AndroidManifest.xml"),
|
|
447
|
-
).toBe(true);
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it("axis 6 — XReal Kotlin source files are present", () => {
|
|
451
|
-
if (!hasAppXr()) return;
|
|
452
|
-
const base = "xreal/app/src/main/java/com/elizaos/facewear/xreal";
|
|
453
|
-
expect(facewearAndroidFileExists(`${base}/MainActivity.kt`)).toBe(true);
|
|
454
|
-
expect(facewearAndroidFileExists(`${base}/CameraService.kt`)).toBe(true);
|
|
455
|
-
expect(facewearAndroidFileExists(`${base}/XrealBridgeJs.kt`)).toBe(true);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it("axis 6 — XReal AndroidManifest declares camera, audio, and XREAL tracking permissions", () => {
|
|
459
|
-
if (!hasAppXr()) return;
|
|
460
|
-
const manifest = readFacewearAndroid(
|
|
461
|
-
"xreal/app/src/main/AndroidManifest.xml",
|
|
462
|
-
);
|
|
463
|
-
expect(manifest).toContain("android.permission.CAMERA");
|
|
464
|
-
expect(manifest).toContain("android.permission.RECORD_AUDIO");
|
|
465
|
-
expect(manifest).toContain("android.permission.INTERNET");
|
|
466
|
-
expect(manifest).toContain("ai.xreal.permission.TRACKING");
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// 7. PWA manifest ───────────────────────────────────────────────────────────
|
|
470
|
-
|
|
471
|
-
it("axis 7 — app-xr has a complete PWA web manifest for browser-based WebXR", () => {
|
|
472
|
-
if (!hasAppXr()) return;
|
|
473
|
-
expect(appXrFileExists("manifest.webmanifest")).toBe(true);
|
|
474
|
-
const manifest = JSON.parse(readAppXr("manifest.webmanifest"));
|
|
475
|
-
expect(manifest.display).toBeDefined();
|
|
476
|
-
expect(manifest.name).toBeDefined();
|
|
477
|
-
expect(manifest.icons?.length).toBeGreaterThan(0);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
// 8. HTTPS tunnel and pairing ───────────────────────────────────────────────
|
|
481
|
-
|
|
482
|
-
it("axis 8 — app-xr package.json has a connect script for HTTPS tunnel + QR code", () => {
|
|
483
|
-
if (!hasAppXr()) return;
|
|
484
|
-
const pkg = JSON.parse(readAppXr("package.json"));
|
|
485
|
-
expect(pkg.scripts?.connect, "connect script for tunnel").toBeDefined();
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
it("axis 8 — xr-connect route serves QR code + text pairing page", () => {
|
|
489
|
-
const routeSrc = readFile("plugins/plugin-xr/src/routes/xr-connect.ts");
|
|
490
|
-
expect(routeSrc).toContain("/xr/connect");
|
|
491
|
-
// Should generate QR code
|
|
492
|
-
expect(routeSrc.toLowerCase()).toContain("qr");
|
|
493
|
-
// Should include a text code fallback
|
|
494
|
-
expect(routeSrc).toContain("code");
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
it("axis 8 — xr-status route provides JSON pairing state for polling", () => {
|
|
498
|
-
const routeSrc = readFile("plugins/plugin-xr/src/routes/xr-status.ts");
|
|
499
|
-
expect(routeSrc).toContain("/xr/");
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
// Cross-cutting: simulator test coverage ────────────────────────────────────
|
|
503
|
-
|
|
504
|
-
it("cross-cut — all-views-crud Playwright spec discovers XR views from the route", () => {
|
|
505
|
-
if (!hasAppXr()) return;
|
|
506
|
-
const specSrc = readAppXr("e2e/all-views-crud.spec.ts");
|
|
507
|
-
expect(specSrc).toContain("/api/xr/views");
|
|
508
|
-
expect(specSrc).not.toContain("ALL_VIEW_IDS");
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
it("cross-cut — voice-forms Playwright spec is present (voice-into-forms routing tested)", () => {
|
|
512
|
-
if (!hasAppXr()) return;
|
|
513
|
-
expect(appXrFileExists("e2e/voice-forms.spec.ts")).toBe(true);
|
|
514
|
-
const src = readAppXr("e2e/voice-forms.spec.ts");
|
|
515
|
-
expect(src).toContain("xr:transcript");
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
it("cross-cut — camera-pose Playwright spec proves DOM overlay is screen-space (panels follow camera)", () => {
|
|
519
|
-
if (!hasAppXr()) return;
|
|
520
|
-
expect(appXrFileExists("e2e/camera-pose.spec.ts")).toBe(true);
|
|
521
|
-
const src = readAppXr("e2e/camera-pose.spec.ts");
|
|
522
|
-
expect(src).toContain("setPose");
|
|
523
|
-
});
|
|
524
|
-
});
|