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 */
2import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3import { tmpdir } from "node:os";
4import { join, resolve } from "node:path";
5import { spawn, type Subprocess } from "bun";
6
7const binary = process.argv[2];
8if (!binary) throw new Error("usage: bun apps/cli/scripts/smoke-packed-apps.ts <packed-binary>");
9
10const AUTH_TOKEN = "packed-apps-smoke-token";
11const READY_TIMEOUT_MS = 45_000;
12
13const fail = (message: string): never => {
14 throw new Error(`[smoke-packed-apps] FAIL: ${message}`);
15};
16
17const 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
31const 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
76const 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
192await main();