@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,129 +0,0 @@
|
|
|
1
|
-
import type { IAgentRuntime } from "@elizaos/core";
|
|
2
|
-
import { ModelType } from "@elizaos/core";
|
|
3
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { AudioPipeline } from "../services/audio-pipeline.ts";
|
|
5
|
-
|
|
6
|
-
function makeRuntime(transcriptResult = "hello world"): IAgentRuntime {
|
|
7
|
-
return {
|
|
8
|
-
useModel: vi.fn().mockResolvedValue(transcriptResult),
|
|
9
|
-
} as unknown as IAgentRuntime;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe("AudioPipeline", () => {
|
|
13
|
-
let onTranscript: ReturnType<typeof vi.fn>;
|
|
14
|
-
let runtime: IAgentRuntime;
|
|
15
|
-
let pipeline: AudioPipeline;
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
onTranscript = vi.fn().mockResolvedValue(undefined);
|
|
19
|
-
runtime = makeRuntime("test transcript");
|
|
20
|
-
pipeline = new AudioPipeline(
|
|
21
|
-
runtime,
|
|
22
|
-
onTranscript as (id: string, text: string) => Promise<void>,
|
|
23
|
-
);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("calls onTranscript after flush with enough audio", async () => {
|
|
27
|
-
const header = {
|
|
28
|
-
type: "audio" as const,
|
|
29
|
-
ts: 0,
|
|
30
|
-
sampleRate: 16000,
|
|
31
|
-
encoding: "webm-opus" as const,
|
|
32
|
-
};
|
|
33
|
-
const chunk = Buffer.alloc(1024, 0x55);
|
|
34
|
-
|
|
35
|
-
pipeline.push("conn1", header, chunk);
|
|
36
|
-
await pipeline.flush("conn1");
|
|
37
|
-
|
|
38
|
-
expect(runtime.useModel).toHaveBeenCalledWith(
|
|
39
|
-
ModelType.TRANSCRIPTION,
|
|
40
|
-
expect.any(Buffer),
|
|
41
|
-
);
|
|
42
|
-
expect(onTranscript).toHaveBeenCalledWith("conn1", "test transcript");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("does not call onTranscript for tiny chunks", async () => {
|
|
46
|
-
const header = {
|
|
47
|
-
type: "audio" as const,
|
|
48
|
-
ts: 0,
|
|
49
|
-
sampleRate: 16000,
|
|
50
|
-
encoding: "webm-opus" as const,
|
|
51
|
-
};
|
|
52
|
-
pipeline.push("conn1", header, Buffer.alloc(64));
|
|
53
|
-
await pipeline.flush("conn1");
|
|
54
|
-
|
|
55
|
-
expect(runtime.useModel).not.toHaveBeenCalled();
|
|
56
|
-
expect(onTranscript).not.toHaveBeenCalled();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("ignores empty transcription results", async () => {
|
|
60
|
-
(runtime.useModel as ReturnType<typeof vi.fn>).mockResolvedValue(" ");
|
|
61
|
-
const header = {
|
|
62
|
-
type: "audio" as const,
|
|
63
|
-
ts: 0,
|
|
64
|
-
sampleRate: 16000,
|
|
65
|
-
encoding: "webm-opus" as const,
|
|
66
|
-
};
|
|
67
|
-
pipeline.push("conn1", header, Buffer.alloc(1024));
|
|
68
|
-
await pipeline.flush("conn1");
|
|
69
|
-
|
|
70
|
-
expect(onTranscript).not.toHaveBeenCalled();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("clears pending state on clear()", () => {
|
|
74
|
-
const header = {
|
|
75
|
-
type: "audio" as const,
|
|
76
|
-
ts: 0,
|
|
77
|
-
sampleRate: 16000,
|
|
78
|
-
encoding: "webm-opus" as const,
|
|
79
|
-
};
|
|
80
|
-
pipeline.push("conn1", header, Buffer.alloc(512));
|
|
81
|
-
pipeline.clear("conn1");
|
|
82
|
-
// after clear, flush has no pending audio to emit
|
|
83
|
-
expect(onTranscript).not.toHaveBeenCalled();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("wraps pcm-f32 chunks with a WAV header before transcription", async () => {
|
|
87
|
-
// pcm-f32: 1024 float32 samples = 4096 bytes of raw audio (> 512-byte floor)
|
|
88
|
-
const float32Samples = new Float32Array(1024).fill(0.1);
|
|
89
|
-
const chunk = Buffer.from(float32Samples.buffer);
|
|
90
|
-
|
|
91
|
-
const header = {
|
|
92
|
-
type: "audio" as const,
|
|
93
|
-
ts: 0,
|
|
94
|
-
sampleRate: 44100,
|
|
95
|
-
encoding: "pcm-f32" as const,
|
|
96
|
-
};
|
|
97
|
-
pipeline.push("conn1", header, chunk);
|
|
98
|
-
await pipeline.flush("conn1");
|
|
99
|
-
|
|
100
|
-
const callArg = (runtime.useModel as ReturnType<typeof vi.fn>).mock
|
|
101
|
-
.calls[0]?.[1] as Buffer;
|
|
102
|
-
|
|
103
|
-
// WAV header starts with "RIFF"
|
|
104
|
-
expect(callArg.subarray(0, 4).toString("ascii")).toBe("RIFF");
|
|
105
|
-
// IEEE_FLOAT format tag = 3 at offset 20 (little-endian uint16)
|
|
106
|
-
expect(callArg.readUInt16LE(20)).toBe(3);
|
|
107
|
-
// sample rate at offset 24
|
|
108
|
-
expect(callArg.readUInt32LE(24)).toBe(44100);
|
|
109
|
-
// data chunk starts at offset 44 — matches original pcm bytes
|
|
110
|
-
expect(callArg.subarray(44)).toEqual(chunk);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("auto-flushes after FLUSH_AFTER_MS duration", async () => {
|
|
114
|
-
const header = {
|
|
115
|
-
type: "audio" as const,
|
|
116
|
-
ts: 0,
|
|
117
|
-
sampleRate: 16000,
|
|
118
|
-
encoding: "webm-opus" as const,
|
|
119
|
-
};
|
|
120
|
-
pipeline.push("conn1", header, Buffer.alloc(1024));
|
|
121
|
-
// Advance ts by 2100ms to trigger auto-flush
|
|
122
|
-
const header2 = { ...header, ts: 2100 };
|
|
123
|
-
pipeline.push("conn1", header2, Buffer.alloc(1024));
|
|
124
|
-
|
|
125
|
-
// flush was called internally; useModel should have been called
|
|
126
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
127
|
-
expect(runtime.useModel).toHaveBeenCalled();
|
|
128
|
-
});
|
|
129
|
-
});
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { decodeBinaryFrame, encodeBinaryFrame } from "../protocol.ts";
|
|
3
|
-
|
|
4
|
-
describe("binary frame codec", () => {
|
|
5
|
-
it("round-trips an audio frame", () => {
|
|
6
|
-
const header = {
|
|
7
|
-
type: "audio" as const,
|
|
8
|
-
ts: 1234567890,
|
|
9
|
-
sampleRate: 48000,
|
|
10
|
-
encoding: "webm-opus" as const,
|
|
11
|
-
};
|
|
12
|
-
const payload = Buffer.from([1, 2, 3, 4, 5]);
|
|
13
|
-
const frame = encodeBinaryFrame(header, payload);
|
|
14
|
-
const decoded = decodeBinaryFrame(frame);
|
|
15
|
-
|
|
16
|
-
expect(decoded.header).toEqual(header);
|
|
17
|
-
expect(decoded.payload).toEqual(payload);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("round-trips a frame with pose data", () => {
|
|
21
|
-
const header = {
|
|
22
|
-
type: "frame" as const,
|
|
23
|
-
ts: 999,
|
|
24
|
-
width: 1280,
|
|
25
|
-
height: 720,
|
|
26
|
-
format: "jpeg" as const,
|
|
27
|
-
pose: {
|
|
28
|
-
position: { x: 1, y: 2, z: 3 },
|
|
29
|
-
orientation: { x: 0, y: 0, z: 0, w: 1 },
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
const payload = Buffer.alloc(100, 0xab);
|
|
33
|
-
const { header: h, payload: p } = decodeBinaryFrame(
|
|
34
|
-
encodeBinaryFrame(header, payload),
|
|
35
|
-
);
|
|
36
|
-
expect(h).toEqual(header);
|
|
37
|
-
expect(p).toEqual(payload);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("handles zero-length payload", () => {
|
|
41
|
-
const header = {
|
|
42
|
-
type: "tts_audio" as const,
|
|
43
|
-
sampleRate: 24000,
|
|
44
|
-
channels: 1,
|
|
45
|
-
encoding: "mp3" as const,
|
|
46
|
-
};
|
|
47
|
-
const { header: h, payload } = decodeBinaryFrame(
|
|
48
|
-
encodeBinaryFrame(header, Buffer.alloc(0)),
|
|
49
|
-
);
|
|
50
|
-
expect(h).toEqual(header);
|
|
51
|
-
expect(payload.length).toBe(0);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route-level e2e for plugin-xr (issue #8802).
|
|
3
|
-
*
|
|
4
|
-
* Boots the plugin's declared `Route[]` through the REAL production dispatcher
|
|
5
|
-
* over a loopback `http.createServer`, exercising the real auth gate, query
|
|
6
|
-
* parsing, param extraction, and `routeHandler` return-shape marshaling — with a
|
|
7
|
-
* faked `XRSessionService` standing in for the only runtime dependency.
|
|
8
|
-
*
|
|
9
|
-
* The XR routes use the canonical return-shape `routeHandler(ctx) -> {status,
|
|
10
|
-
* headers, body}` contract. In production those routes are dispatched by
|
|
11
|
-
* `mountRoutesOnHono` -> `dispatchRoute` (packages/agent/src/api/hono-adapter.ts
|
|
12
|
-
* + dispatch-route.ts), NOT by the legacy `tryHandleRuntimePluginRoute` (which
|
|
13
|
-
* only invokes the Express-shaped `route.handler`). This test drives the actual
|
|
14
|
-
* production path: it builds the real Hono app for the runtime and serves its
|
|
15
|
-
* `fetch` handler over a real TCP socket, asserting on live HTTP responses.
|
|
16
|
-
*
|
|
17
|
-
* No mocked `json`/`error` helpers, no shape-only checks: every assertion is on
|
|
18
|
-
* a real HTTP response delivered over the loopback socket.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { Buffer } from "node:buffer";
|
|
22
|
-
import http from "node:http";
|
|
23
|
-
import type { AddressInfo } from "node:net";
|
|
24
|
-
import type { IAgentRuntime } from "@elizaos/core";
|
|
25
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
26
|
-
|
|
27
|
-
import { buildHonoAppForRuntime } from "../../../../packages/agent/src/api/hono-adapter.ts";
|
|
28
|
-
import { xrConnectRoute } from "../routes/xr-connect.ts";
|
|
29
|
-
import { xrSimulatorRoute } from "../routes/xr-simulator-route.ts";
|
|
30
|
-
import { xrStatusRoute } from "../routes/xr-status.ts";
|
|
31
|
-
import { xrViewHostRoute } from "../routes/xr-view-host.ts";
|
|
32
|
-
import { xrViewsRoute } from "../routes/xr-views.ts";
|
|
33
|
-
import { XR_SERVICE_TYPE } from "../services/xr-session-service.ts";
|
|
34
|
-
|
|
35
|
-
const XR_ROUTES = [
|
|
36
|
-
xrStatusRoute,
|
|
37
|
-
xrConnectRoute,
|
|
38
|
-
xrViewsRoute,
|
|
39
|
-
xrViewHostRoute,
|
|
40
|
-
xrSimulatorRoute,
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
const servers: http.Server[] = [];
|
|
44
|
-
|
|
45
|
-
afterEach(async () => {
|
|
46
|
-
await Promise.all(
|
|
47
|
-
servers.map(
|
|
48
|
-
(server) =>
|
|
49
|
-
new Promise<void>((resolve) => {
|
|
50
|
-
server.closeAllConnections?.();
|
|
51
|
-
server.close(() => resolve());
|
|
52
|
-
}),
|
|
53
|
-
),
|
|
54
|
-
);
|
|
55
|
-
servers.length = 0;
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
interface FakeConnection {
|
|
59
|
-
id: string;
|
|
60
|
-
deviceType: string;
|
|
61
|
-
connectedAt: Date;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function makeRuntime(
|
|
65
|
-
options: {
|
|
66
|
-
withService?: boolean;
|
|
67
|
-
connections?: FakeConnection[];
|
|
68
|
-
recentFrameIds?: string[];
|
|
69
|
-
} = {},
|
|
70
|
-
): IAgentRuntime {
|
|
71
|
-
const { withService = true, connections = [], recentFrameIds = [] } = options;
|
|
72
|
-
|
|
73
|
-
const recent = new Set(recentFrameIds);
|
|
74
|
-
const service = {
|
|
75
|
-
getConnections: () => connections,
|
|
76
|
-
getVisionPipeline: () => ({
|
|
77
|
-
hasRecentFrame: (id: string) => recent.has(id),
|
|
78
|
-
}),
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
routes: XR_ROUTES,
|
|
83
|
-
getService: (key: string) =>
|
|
84
|
-
withService && key === XR_SERVICE_TYPE ? service : null,
|
|
85
|
-
// xrViewHostRoute reads `runtime.port` to build the bundle origin.
|
|
86
|
-
port: 31337,
|
|
87
|
-
} as unknown as IAgentRuntime;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Serve the real production Hono app (built from `runtime.routes`) over a real
|
|
92
|
-
* loopback TCP socket. Bridges the Node request/response to Hono's standard
|
|
93
|
-
* `fetch` handler so assertions run against genuine HTTP — not an in-memory
|
|
94
|
-
* handler invocation.
|
|
95
|
-
*/
|
|
96
|
-
async function startServer(
|
|
97
|
-
runtime: IAgentRuntime,
|
|
98
|
-
isAuthorized: () => boolean = () => true,
|
|
99
|
-
): Promise<string> {
|
|
100
|
-
const app = buildHonoAppForRuntime(runtime, { isAuthorized });
|
|
101
|
-
|
|
102
|
-
const server = http.createServer((req, res) => {
|
|
103
|
-
const chunks: Buffer[] = [];
|
|
104
|
-
req.on("data", (chunk) => chunks.push(chunk as Buffer));
|
|
105
|
-
req.on("end", () => {
|
|
106
|
-
void (async () => {
|
|
107
|
-
const url = `http://127.0.0.1${req.url ?? "/"}`;
|
|
108
|
-
const method = req.method ?? "GET";
|
|
109
|
-
const headers = new Headers();
|
|
110
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
111
|
-
if (value === undefined) continue;
|
|
112
|
-
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
|
|
113
|
-
}
|
|
114
|
-
const hasBody = method !== "GET" && method !== "HEAD";
|
|
115
|
-
const request = new Request(url, {
|
|
116
|
-
method,
|
|
117
|
-
headers,
|
|
118
|
-
body: hasBody && chunks.length ? Buffer.concat(chunks) : undefined,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const response = await app.fetch(request);
|
|
122
|
-
|
|
123
|
-
res.statusCode = response.status;
|
|
124
|
-
response.headers.forEach((value, key) => {
|
|
125
|
-
res.setHeader(key, value);
|
|
126
|
-
});
|
|
127
|
-
const buf = Buffer.from(await response.arrayBuffer());
|
|
128
|
-
res.end(buf);
|
|
129
|
-
})();
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
servers.push(server);
|
|
134
|
-
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
135
|
-
const { port } = server.address() as AddressInfo;
|
|
136
|
-
return `http://127.0.0.1:${port}`;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
describe("plugin-xr routes (real dispatch)", () => {
|
|
140
|
-
it("serves /xr/status with the connected-device list when the service is present", async () => {
|
|
141
|
-
const connectedAt = new Date("2024-01-01T00:00:00.000Z");
|
|
142
|
-
const base = await startServer(
|
|
143
|
-
makeRuntime({
|
|
144
|
-
connections: [
|
|
145
|
-
{ id: "conn-a", deviceType: "quest3", connectedAt },
|
|
146
|
-
{ id: "conn-b", deviceType: "xreal", connectedAt },
|
|
147
|
-
],
|
|
148
|
-
recentFrameIds: ["conn-a"],
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
const res = await fetch(`${base}/xr/status`);
|
|
153
|
-
expect(res.status).toBe(200);
|
|
154
|
-
expect(res.headers.get("content-type")).toContain("application/json");
|
|
155
|
-
|
|
156
|
-
const body = (await res.json()) as {
|
|
157
|
-
connected: boolean;
|
|
158
|
-
connections: Array<{
|
|
159
|
-
id: string;
|
|
160
|
-
deviceType: string;
|
|
161
|
-
connectedAt: string;
|
|
162
|
-
hasRecentFrame: boolean;
|
|
163
|
-
}>;
|
|
164
|
-
};
|
|
165
|
-
expect(body.connected).toBe(true);
|
|
166
|
-
expect(body.connections).toHaveLength(2);
|
|
167
|
-
expect(body.connections[0]).toEqual({
|
|
168
|
-
id: "conn-a",
|
|
169
|
-
deviceType: "quest3",
|
|
170
|
-
connectedAt: "2024-01-01T00:00:00.000Z",
|
|
171
|
-
hasRecentFrame: true,
|
|
172
|
-
});
|
|
173
|
-
expect(body.connections[1].hasRecentFrame).toBe(false);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("reports connected:false when the service has no active connections", async () => {
|
|
177
|
-
const base = await startServer(makeRuntime({ connections: [] }));
|
|
178
|
-
const res = await fetch(`${base}/xr/status`);
|
|
179
|
-
expect(res.status).toBe(200);
|
|
180
|
-
const body = (await res.json()) as {
|
|
181
|
-
connected: boolean;
|
|
182
|
-
connections: unknown[];
|
|
183
|
-
};
|
|
184
|
-
expect(body.connected).toBe(false);
|
|
185
|
-
expect(body.connections).toEqual([]);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("returns 503 from /xr/status when the XR service is unavailable", async () => {
|
|
189
|
-
const base = await startServer(makeRuntime({ withService: false }));
|
|
190
|
-
const res = await fetch(`${base}/xr/status`);
|
|
191
|
-
expect(res.status).toBe(503);
|
|
192
|
-
const body = (await res.json()) as { error: string };
|
|
193
|
-
expect(body.error).toContain("XR service not running");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("enforces the auth gate on the non-public /xr/status route", async () => {
|
|
197
|
-
const base = await startServer(makeRuntime(), () => false);
|
|
198
|
-
const res = await fetch(`${base}/xr/status`);
|
|
199
|
-
expect(res.status).toBe(401);
|
|
200
|
-
expect(((await res.json()) as { error: string }).error).toBe(
|
|
201
|
-
"Unauthorized",
|
|
202
|
-
);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("serves /xr/connect as an HTML pairing page when authorized", async () => {
|
|
206
|
-
const base = await startServer(makeRuntime());
|
|
207
|
-
const res = await fetch(`${base}/xr/connect`);
|
|
208
|
-
expect(res.status).toBe(200);
|
|
209
|
-
expect(res.headers.get("content-type")).toContain("text/html");
|
|
210
|
-
const html = await res.text();
|
|
211
|
-
expect(html).toContain("<!DOCTYPE html>");
|
|
212
|
-
expect(html).toContain("Connect XR Headset");
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("auth-gates /xr/connect when the request is unauthorized", async () => {
|
|
216
|
-
const base = await startServer(makeRuntime(), () => false);
|
|
217
|
-
const res = await fetch(`${base}/xr/connect`);
|
|
218
|
-
expect(res.status).toBe(401);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("serves /xr/views with views, count, and live connections", async () => {
|
|
222
|
-
const base = await startServer(
|
|
223
|
-
makeRuntime({
|
|
224
|
-
connections: [
|
|
225
|
-
{
|
|
226
|
-
id: "conn-a",
|
|
227
|
-
deviceType: "quest3",
|
|
228
|
-
connectedAt: new Date("2024-01-01T00:00:00.000Z"),
|
|
229
|
-
},
|
|
230
|
-
],
|
|
231
|
-
}),
|
|
232
|
-
);
|
|
233
|
-
const res = await fetch(`${base}/xr/views`);
|
|
234
|
-
expect(res.status).toBe(200);
|
|
235
|
-
const body = (await res.json()) as {
|
|
236
|
-
views: unknown[];
|
|
237
|
-
count: number;
|
|
238
|
-
connections: Array<{ id: string; deviceType: string }>;
|
|
239
|
-
};
|
|
240
|
-
expect(Array.isArray(body.views)).toBe(true);
|
|
241
|
-
expect(body.count).toBe(body.views.length);
|
|
242
|
-
expect(body.connections).toEqual([{ id: "conn-a", deviceType: "quest3" }]);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it("serves /xr/view-host/:id HTML with the extracted view id", async () => {
|
|
246
|
-
const base = await startServer(makeRuntime());
|
|
247
|
-
const res = await fetch(`${base}/xr/view-host/wallet`);
|
|
248
|
-
expect(res.status).toBe(200);
|
|
249
|
-
expect(res.headers.get("content-type")).toContain("text/html");
|
|
250
|
-
const html = await res.text();
|
|
251
|
-
expect(html).toContain('data-view-id="wallet"');
|
|
252
|
-
expect(html).toContain("/api/views/wallet/bundle.js");
|
|
253
|
-
expect(res.headers.get("content-security-policy")).toContain("esm.sh");
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it("serves /xr/simulator.js according to whether the emulator bundle is built", async () => {
|
|
257
|
-
const base = await startServer(makeRuntime());
|
|
258
|
-
const res = await fetch(`${base}/xr/simulator.js`);
|
|
259
|
-
|
|
260
|
-
// The route's two real branches: 200 + JS when the simulator bundle has
|
|
261
|
-
// been built, 404 + a build hint otherwise. Both are exercised here over
|
|
262
|
-
// real HTTP; which one fires depends on whether `simulator:build` has run.
|
|
263
|
-
if (res.status === 200) {
|
|
264
|
-
expect(res.headers.get("content-type")).toContain(
|
|
265
|
-
"application/javascript",
|
|
266
|
-
);
|
|
267
|
-
const js = await res.text();
|
|
268
|
-
expect(js.length).toBeGreaterThan(0);
|
|
269
|
-
} else {
|
|
270
|
-
expect(res.status).toBe(404);
|
|
271
|
-
const body = (await res.json()) as { error: string; hint: string };
|
|
272
|
-
expect(body.error).toContain("Emulator bundle not built");
|
|
273
|
-
expect(body.hint).toContain("simulator");
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
});
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { IAgentRuntime } from "@elizaos/core";
|
|
2
|
-
import { ModelType } from "@elizaos/core";
|
|
3
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { VisionPipeline } from "../services/vision-pipeline.ts";
|
|
5
|
-
|
|
6
|
-
function makeRuntime(result = "a desk with a laptop"): IAgentRuntime {
|
|
7
|
-
return {
|
|
8
|
-
useModel: vi.fn().mockResolvedValue(result),
|
|
9
|
-
} as unknown as IAgentRuntime;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const FRAME_HEADER = {
|
|
13
|
-
type: "frame" as const,
|
|
14
|
-
ts: Date.now(),
|
|
15
|
-
width: 1280,
|
|
16
|
-
height: 720,
|
|
17
|
-
format: "jpeg" as const,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
describe("VisionPipeline", () => {
|
|
21
|
-
let pipeline: VisionPipeline;
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
pipeline = new VisionPipeline();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("stores and retrieves the latest frame", () => {
|
|
28
|
-
const data = Buffer.alloc(512, 0xff);
|
|
29
|
-
pipeline.storeFrame("conn1", FRAME_HEADER, data);
|
|
30
|
-
const frame = pipeline.getLatestFrame("conn1");
|
|
31
|
-
expect(frame).toBeDefined();
|
|
32
|
-
expect(frame?.data).toBe(data);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("returns undefined for an unknown connection", () => {
|
|
36
|
-
expect(pipeline.getLatestFrame("unknown")).toBeUndefined();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("clears frames on clear()", () => {
|
|
40
|
-
pipeline.storeFrame("conn1", FRAME_HEADER, Buffer.alloc(128));
|
|
41
|
-
pipeline.clear("conn1");
|
|
42
|
-
expect(pipeline.getLatestFrame("conn1")).toBeUndefined();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("describeFrame calls IMAGE_DESCRIPTION model with data URL", async () => {
|
|
46
|
-
const runtime = makeRuntime("a red chair");
|
|
47
|
-
const data = Buffer.from([0xff, 0xd8, 0xff]); // Minimal JPEG header bytes.
|
|
48
|
-
pipeline.storeFrame("conn1", FRAME_HEADER, data);
|
|
49
|
-
|
|
50
|
-
const result = await pipeline.describeFrame(runtime, "conn1");
|
|
51
|
-
|
|
52
|
-
expect(runtime.useModel).toHaveBeenCalledWith(
|
|
53
|
-
ModelType.IMAGE_DESCRIPTION,
|
|
54
|
-
expect.objectContaining({
|
|
55
|
-
imageUrl: expect.stringContaining("data:image/jpeg;base64,"),
|
|
56
|
-
}),
|
|
57
|
-
);
|
|
58
|
-
expect(result).toBe("a red chair");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("describeFrame returns null when no frame exists", async () => {
|
|
62
|
-
const runtime = makeRuntime();
|
|
63
|
-
const result = await pipeline.describeFrame(runtime, "conn1");
|
|
64
|
-
expect(result).toBeNull();
|
|
65
|
-
expect(runtime.useModel).not.toHaveBeenCalled();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("hasRecentFrame returns true when frame exists", () => {
|
|
69
|
-
pipeline.storeFrame("conn1", FRAME_HEADER, Buffer.alloc(64));
|
|
70
|
-
expect(pipeline.hasRecentFrame("conn1")).toBe(true);
|
|
71
|
-
expect(pipeline.hasRecentFrame("conn2")).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
});
|