History

Package workerd for local binaries

Rhys Sullivan committed 4 months ago

Commit 4be58bc1 parent da4f37c8 files · +491 −56Browse files

Showing the changes to apps/cli/scripts (1 of 8 files). Show all

Unified · Split

apps/cli/scripts/smoke-packed-apps.ts+192−0
@@ -0,0 +1,192 @@
1+/* oxlint-disable executor/no-try-catch-or-throw, executor/no-error-constructor, executor/no-promise-reject, executor/no-promise-catch -- boundary: standalone build smoke harness tears down a spawned packed binary */
2+import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3+import { tmpdir } from "node:os";
4+import { join, resolve } from "node:path";
5+import { spawn, type Subprocess } from "bun";
6+
7+const binary = process.argv[2];
8+if (!binary) throw new Error("usage: bun apps/cli/scripts/smoke-packed-apps.ts <packed-binary>");
9+
10+const AUTH_TOKEN = "packed-apps-smoke-token";
11+const READY_TIMEOUT_MS = 45_000;
12+
13+const fail = (message: string): never => {
14+ throw new Error(`[smoke-packed-apps] FAIL: ${message}`);
15+};
16+
17+const requestJson = async <T>(origin: string, path: string, init: RequestInit = {}): Promise<T> => {
18+ const response = await fetch(new URL(path, origin), {
19+ ...init,
20+ headers: {
21+ authorization: `Bearer ${AUTH_TOKEN}`,
22+ ...(init.body === undefined ? {} : { "content-type": "application/json" }),
23+ ...init.headers,
24+ },
25+ });
26+ const text = await response.text();
27+ if (!response.ok) fail(`${init.method ?? "GET"} ${path} returned ${response.status}: ${text}`);
28+ return (text.length > 0 ? JSON.parse(text) : null) as T;
29+};
30+
31+const waitForReadyPort = async (proc: Subprocess<"ignore", "pipe", "pipe">): Promise<number> =>
32+ new Promise((resolveReady, rejectReady) => {
33+ const timer = setTimeout(
34+ () => rejectReady(new Error("packed binary did not report ready in time")),
35+ READY_TIMEOUT_MS,
36+ );
37+ let stdout = "";
38+ let stderr = "";
39+ const finish = (result: number | Error): void => {
40+ clearTimeout(timer);
41+ if (result instanceof Error) rejectReady(result);
42+ else resolveReady(result);
43+ };
44+ void proc.exited.then((code) => {
45+ finish(new Error(`packed binary exited before ready with code ${code}: ${stderr}`));
46+ });
47+ void (async () => {
48+ const reader = proc.stdout.getReader();
49+ const decoder = new TextDecoder();
50+ try {
51+ while (true) {
52+ const chunk = await reader.read();
53+ if (chunk.done) return;
54+ stdout += decoder.decode(chunk.value);
55+ const match = stdout.match(/EXECUTOR_READY:(\d+)/);
56+ if (match) {
57+ finish(Number(match[1]));
58+ return;
59+ }
60+ }
61+ } catch (cause) {
62+ finish(cause instanceof Error ? cause : new Error(String(cause)));
63+ }
64+ })();
65+ void (async () => {
66+ const reader = proc.stderr.getReader();
67+ const decoder = new TextDecoder();
68+ while (true) {
69+ const chunk = await reader.read();
70+ if (chunk.done) return;
71+ stderr += decoder.decode(chunk.value);
72+ }
73+ })().catch(() => undefined);
74+ });
75+
76+const main = async (): Promise<void> => {
77+ const fixture = await mkdtemp(join(tmpdir(), "executor-packed-apps-fixture-"));
78+ const dataDir = await mkdtemp(join(tmpdir(), "executor-packed-apps-data-"));
79+ const scopeDir = await mkdtemp(join(tmpdir(), "executor-packed-apps-scope-"));
80+ let proc: Subprocess<"ignore", "pipe", "pipe"> | null = null;
81+ try {
82+ await mkdir(join(fixture, "tools"), { recursive: true });
83+ await writeFile(
84+ join(fixture, "tools", "greet.ts"),
85+ `
86+ import { z } from "zod";
87+ import { defineTool } from "executor:app";
88+
89+ const Input = z.object({ name: z.string() });
90+
91+ export default defineTool({
92+ description: "Greet",
93+ input: Input,
94+ async handler(input) {
95+ return { greeting: "hello " + input.name };
96+ },
97+ });
98+ `,
99+ );
100+ await writeFile(
101+ join(fixture, "package.json"),
102+ `${JSON.stringify({ name: "packed-apps-smoke", dependencies: { zod: "4.3.6" } })}\n`,
103+ );
104+ await writeFile(join(fixture, "bun.lock"), "");
105+
106+ proc = spawn({
107+ cmd: [
108+ resolve(binary),
109+ "daemon",
110+ "run",
111+ "--foreground",
112+ "--port",
113+ "0",
114+ "--hostname",
115+ "127.0.0.1",
116+ "--auth-token",
117+ AUTH_TOKEN,
118+ ],
119+ env: {
120+ ...process.env,
121+ EXECUTOR_CLIENT: "desktop",
122+ EXECUTOR_DATA_DIR: dataDir,
123+ EXECUTOR_SCOPE_DIR: scopeDir,
124+ },
125+ stdin: "ignore",
126+ stdout: "pipe",
127+ stderr: "pipe",
128+ });
129+
130+ const port = await waitForReadyPort(proc);
131+ const origin = `http://127.0.0.1:${port}`;
132+ console.log(`[smoke-packed-apps] ready on ${origin}`);
133+
134+ await requestJson(origin, "/api/apps/sources", {
135+ method: "POST",
136+ body: JSON.stringify({
137+ kind: "local-directory",
138+ slug: "packed-apps-smoke",
139+ app: "packed-apps-smoke",
140+ path: fixture,
141+ }),
142+ });
143+
144+ const sync = await requestJson<{
145+ readonly status: string;
146+ readonly tools: readonly string[];
147+ readonly errors?: readonly unknown[];
148+ }>(origin, "/api/apps/sources/packed-apps-smoke/sync", { method: "POST" });
149+ if (sync.status !== "published") fail(`sync failed: ${JSON.stringify(sync.errors)}`);
150+ if (JSON.stringify(sync.tools) !== JSON.stringify(["greet"])) {
151+ fail(`expected greet tool, got ${JSON.stringify(sync.tools)}`);
152+ }
153+
154+ const invoked = await requestJson<{
155+ readonly status: string;
156+ readonly structured: unknown;
157+ readonly isError: boolean;
158+ readonly text: string;
159+ }>(origin, "/api/executions", {
160+ method: "POST",
161+ body: JSON.stringify({
162+ code: `export default await tools["packed-apps-smoke.org.published.greet"]({ name: "Ada" })`,
163+ autoApprove: true,
164+ }),
165+ });
166+ if (invoked.status !== "completed" || invoked.isError) {
167+ fail(`execution failed: ${JSON.stringify(invoked)}`);
168+ }
169+ if (
170+ JSON.stringify(invoked.structured) !==
171+ JSON.stringify({
172+ status: "completed",
173+ result: { ok: true, data: { greeting: "hello Ada" } },
174+ logs: [],
175+ })
176+ ) {
177+ fail(`unexpected execution result: ${JSON.stringify(invoked)}`);
178+ }
179+ console.log("[smoke-packed-apps] OK - local-directory app source bundled and invoked");
180+ } finally {
181+ if (proc) {
182+ proc.kill("SIGTERM");
183+ await Promise.race([proc.exited, new Promise((resolve) => setTimeout(resolve, 3000))]);
184+ proc.kill("SIGKILL");
185+ }
186+ await rm(fixture, { recursive: true, force: true }).catch(() => undefined);
187+ await rm(dataDir, { recursive: true, force: true }).catch(() => undefined);
188+ await rm(scopeDir, { recursive: true, force: true }).catch(() => undefined);
189+ }
190+};
191+
192+await main();