@lizard-build/cli 4.0.26 → 4.0.27

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.
@@ -0,0 +1,112 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import http from "node:http";
3
+ import net from "node:net";
4
+ import { once } from "node:events";
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { Command } from "commander";
9
+ import { WebSocketServer } from "ws";
10
+ import { execStream, registerSandbox } from "../../src/commands/sandbox.js";
11
+ import { getBaseURL, setBaseURL, setAccessToken } from "../../src/lib/api.js";
12
+ import { startVncTunnel, vncTarget } from "../../src/commands/sandbox-vnc.js";
13
+ vi.mock("../../src/lib/format.js", () => ({ isJSONMode: () => true, printJSON: vi.fn() }));
14
+
15
+ const originalURL = getBaseURL();
16
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "sandbox-transport-"));
17
+ let respond: (req: http.IncomingMessage, res: http.ServerResponse) => void;
18
+ const server = http.createServer((req, res) => respond(req, res));
19
+ let base: string;
20
+ beforeAll(async () => {
21
+ server.listen(0, "127.0.0.1"); await once(server, "listening");
22
+ base = `http://127.0.0.1:${(server.address() as net.AddressInfo).port}`;
23
+ setBaseURL(base); setAccessToken("test-token");
24
+ });
25
+ afterAll(async () => { setBaseURL(originalURL); setAccessToken(""); await new Promise<void>(r => server.close(() => r())); fs.rmSync(tmp, { recursive: true }); });
26
+ beforeEach(() => { respond = (_req, res) => res.end(); });
27
+ function command(args: string[]) {
28
+ const program = new Command().exitOverride().configureOutput({ writeErr: () => {} });
29
+ registerSandbox(program);
30
+ return program.parseAsync(["sandbox", ...args], { from: "user" });
31
+ }
32
+
33
+ describe("sandbox exec stream", () => {
34
+ it("decodes split UTF-8, CRLF and a final exit without a newline", async () => {
35
+ const output: [string, string][] = [];
36
+ respond = (req, res) => {
37
+ expect(req.headers.authorization).toBe("Bearer test-token");
38
+ expect(req.headers["user-agent"]).toMatch(/^lizard-cli\//);
39
+ res.setHeader("Content-Type", "text/event-stream");
40
+ const bytes = Buffer.from('event: output\r\ndata: {"stream":"stderr",\r\ndata: "line":"привет"}\r\n\r\nevent: exit\ndata: {"exitCode":7}');
41
+ for (let i = 0; i < bytes.length; i++) res.write(bytes.subarray(i, i + 1));
42
+ res.end();
43
+ };
44
+ expect(await execStream("sb", "test", (s, l) => output.push([s, l]))).toBe(7);
45
+ expect(output).toEqual([["stderr", "привет"]]);
46
+ });
47
+ it.each(["", 'event: output\ndata: {"line":"partial"}\n\n'])("rejects an incomplete stream", async body => {
48
+ respond = (_req, res) => res.end(body);
49
+ await expect(execStream("sb", "test", () => {})).rejects.toMatchObject({ code: "EXEC_STREAM_INCOMPLETE" });
50
+ });
51
+ it.each([null, -1, 256, 1.5, "0"])("rejects invalid exit code %s", async exitCode => {
52
+ respond = (_req, res) => res.end(`event: exit\ndata: ${JSON.stringify({ exitCode })}\n\n`);
53
+ await expect(execStream("sb", "test", () => {})).rejects.toThrow("Invalid exec exit event");
54
+ });
55
+ it.each([401, 403, 404, 408, 504])("preserves HTTP %i as an APIError", async status => {
56
+ respond = (_req, res) => { res.writeHead(status); res.end(JSON.stringify({ error: "test failure", code: "TEST" })); };
57
+ await expect(execStream("sb", "test", () => {})).rejects.toMatchObject({ status, message: "test failure", code: "TEST" });
58
+ });
59
+ it("accepts a complete success", async () => {
60
+ respond = (_req, res) => res.end('event: exit\ndata: {"exitCode":0}\n\n');
61
+ expect(await execStream("sb", "true", () => {})).toBe(0);
62
+ });
63
+ });
64
+
65
+ describe("binary files", () => {
66
+ it.each([Buffer.from([0, 255, 128, 13, 10, 195, 40]), Buffer.alloc(0)])("round-trips arbitrary bytes", async bytes => {
67
+ const input = path.join(tmp, "input.bin"), output = path.join(tmp, "output.bin");
68
+ fs.writeFileSync(input, bytes);
69
+ let stored = Buffer.alloc(0);
70
+ respond = (req, res) => {
71
+ if (req.method === "POST") {
72
+ let body = ""; req.on("data", b => body += b); req.on("end", () => {
73
+ const data = JSON.parse(body); expect(data.encoding).toBe("base64");
74
+ expect(data.path).toBe("/workspace/binary"); stored = Buffer.from(data.content, data.encoding);
75
+ res.setHeader("Content-Type", "application/json"); res.end('{"status":"written"}');
76
+ });
77
+ } else { res.setHeader("Content-Type", "application/octet-stream"); res.end(stored); }
78
+ };
79
+ await command(["files", "put", "sb", input, "/workspace/binary"]);
80
+ await command(["files", "get", "sb", "/workspace/binary", output]);
81
+ expect(fs.readFileSync(output)).toEqual(bytes);
82
+ });
83
+ });
84
+
85
+ describe("ports", () => {
86
+ it.each(["3000oops", "3.5", "0", "65536", "-1"])("rejects %s before a request", async port => {
87
+ let requests = 0; respond = (_req, res) => { requests++; res.end('{}'); };
88
+ await expect(command(["vnc", "sb", "--port", port])).rejects.toThrow(/Port must be/);
89
+ expect(requests).toBe(0);
90
+ });
91
+ });
92
+
93
+ describe("private VNC tunnel", () => {
94
+ it.each(["control", "view"])("forwards both tokens for %s sessions", async token => {
95
+ const wss = new WebSocketServer({ port: 0, host: "127.0.0.1", verifyClient: info => info.req.headers["x-lizard-access-token"] === "private" });
96
+ await once(wss, "listening");
97
+ const port = (wss.address() as net.AddressInfo).port;
98
+ const target = vncTarget(`http://127.0.0.1:${port}/stream.html?token=${token}&password=pw&lizard_token=private`);
99
+ let upstreamPath = "";
100
+ wss.on("connection", (ws, req) => { upstreamPath = req.url!; ws.send(Buffer.from("RFB 003.008\n")); });
101
+ const tunnel = await startVncTunnel(target.wsUrl, 19590, {}, target.headers);
102
+ const sock = net.connect(tunnel.port, "127.0.0.1");
103
+ try {
104
+ const [bytes] = await once(sock, "data");
105
+ expect(bytes.toString()).toBe("RFB 003.008\n");
106
+ expect(upstreamPath).toBe(`/websockify?token=${token}`);
107
+ } finally { sock.destroy(); tunnel.close(); for (const ws of wss.clients) ws.terminate(); await new Promise<void>(r => wss.close(() => r())); }
108
+ });
109
+ it("supports older noVNC URLs", () => {
110
+ expect(vncTarget("https://desktop/vnc.html?path=websockify%3Ftoken%3Da%252Bb&password=pw")).toEqual({ wsUrl: "wss://desktop/websockify?token=a%2Bb", password: "pw", headers: {} });
111
+ });
112
+ });