@byollm/conformance 0.1.0-alpha.0

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,211 @@
1
+ /**
2
+ * Certify the Supabase adapter with the conformance kit.
3
+ *
4
+ * Run against a local stack:
5
+ *
6
+ * ```bash
7
+ * cd packages/server && supabase start
8
+ * pnpm --filter @byollm/server run conformance:supabase
9
+ * ```
10
+ *
11
+ * "A server is byollm-compatible when the kit passes" — this script is that
12
+ * sentence applied to the first-party adapter. It is the same kit, the same
13
+ * checks and the same real daemon that certify the in-memory reference, so a
14
+ * behaviour the two stores disagree about fails here rather than in someone's
15
+ * production queue.
16
+ */
17
+ import { randomUUID } from "node:crypto";
18
+ import { createClient } from "@supabase/supabase-js";
19
+ import {
20
+ certify,
21
+ formatReport,
22
+ type ConformanceTarget,
23
+ } from "@byollm/conformance";
24
+ // The built package, not `../src`: Node's type stripping does not rewrite the
25
+ // `.js` specifiers the source uses, and certifying the published entry points
26
+ // is closer to what a consumer actually gets.
27
+ import { ByollmApp, createFetchHandler } from "@byollm/server";
28
+ import { supabaseStore } from "@byollm/server/supabase";
29
+
30
+ const SUPABASE_URL = process.env["SUPABASE_URL"] ?? "http://127.0.0.1:54421";
31
+ const SERVICE_KEY =
32
+ process.env["SUPABASE_SERVICE_ROLE_KEY"] ??
33
+ process.env["SUPABASE_SECRET_KEY"] ??
34
+ "";
35
+
36
+ const ORIGIN = "https://supabase.byollm.test";
37
+ /** Short, because a real Postgres clock cannot be faked forward. */
38
+ const LEASE_MS = 2_000;
39
+ const TTL_MS = 1_500;
40
+
41
+ if (SERVICE_KEY === "") {
42
+ process.stderr.write(
43
+ "SUPABASE_SERVICE_ROLE_KEY (or SUPABASE_SECRET_KEY) is required.\n" +
44
+ "Run `supabase status` in packages/server to find it.\n",
45
+ );
46
+ process.exit(2);
47
+ }
48
+
49
+ // `createClient` infers a wider generic than the bare `SupabaseClient` alias;
50
+ // letting it infer avoids an assignment the linter cannot verify.
51
+ const client = createClient(SUPABASE_URL, SERVICE_KEY, {
52
+ auth: { persistSession: false, autoRefreshToken: false },
53
+ });
54
+
55
+ /**
56
+ * The kit talks about owners as names ("alice"); Postgres needs uuids that
57
+ * reference `auth.users`. This maps between them so the checks read the same
58
+ * against both stores.
59
+ */
60
+ const owners = new Map<string, string>();
61
+ const ownerNames = new Map<string, string>();
62
+
63
+ /**
64
+ * Per-run email suffix.
65
+ *
66
+ * Users are created fresh each run rather than looked up, because
67
+ * `auth.admin.listUsers` is unreliable on the local stack ("Database error
68
+ * finding users") and a certification run must not depend on it. Unique
69
+ * emails mean `createUser` always succeeds and never needs a fallback.
70
+ */
71
+ const RUN = randomUUID().slice(0, 8);
72
+
73
+ async function ensureUser(name: string): Promise<string> {
74
+ const existing = owners.get(name);
75
+ if (existing !== undefined) return existing;
76
+
77
+ const { data, error } = await client.auth.admin.createUser({
78
+ email: `${name}+${RUN}@byollm.test`,
79
+ email_confirm: true,
80
+ });
81
+ if (error)
82
+ throw new Error(`could not create test user ${name}: ${error.message}`);
83
+
84
+ const id = data.user.id;
85
+ owners.set(name, id);
86
+ ownerNames.set(id, name);
87
+ return id;
88
+ }
89
+
90
+ const toName = (id: string): string => ownerNames.get(id) ?? id;
91
+
92
+ const store = supabaseStore({ client, defaultTtlMs: TTL_MS });
93
+ const app = new ByollmApp({ store });
94
+ const handler = createFetchHandler({
95
+ store,
96
+ verificationUrl: `${ORIGIN}/settings/runners`,
97
+ leaseMs: LEASE_MS,
98
+ });
99
+
100
+ const target: ConformanceTarget = {
101
+ name: "@byollm/server (Supabase adapter)",
102
+ origin: ORIGIN,
103
+ leaseMs: LEASE_MS,
104
+ ttlMs: TTL_MS,
105
+ // No `advanceTime`: a real Postgres clock cannot be moved, so the kit waits
106
+ // for real. That is why the lease and TTL above are short.
107
+
108
+ fetch: (request) => handler(request),
109
+
110
+ enqueue: async (input) => {
111
+ const handle = await app.enqueue({
112
+ kind: input.kind,
113
+ payload: input.payload,
114
+ owner: await ensureUser(input.owner),
115
+ ...(input.audience === undefined ? {} : { audience: input.audience }),
116
+ ...(input.audienceAllow === undefined
117
+ ? {}
118
+ : {
119
+ audienceAllow: await Promise.all(
120
+ input.audienceAllow.map((name) => ensureUser(name)),
121
+ ),
122
+ }),
123
+ ...(input.dependsOn === undefined ? {} : { dependsOn: input.dependsOn }),
124
+ ...(input.ttlMs === undefined ? {} : { ttlMs: input.ttlMs }),
125
+ });
126
+ return { id: handle.id };
127
+ },
128
+
129
+ approvePairing: async (userCode, owner) => {
130
+ await app.approvePairing({ userCode, owner: await ensureUser(owner) });
131
+ },
132
+
133
+ revokeRunner: (runnerId) => app.revokeRunner(runnerId),
134
+
135
+ cancelJob: async (jobId) => {
136
+ await app.cancel(jobId);
137
+ },
138
+
139
+ job: async (jobId) => {
140
+ const record = await app.job(jobId);
141
+ if (!record) return null;
142
+ return {
143
+ state: record.state,
144
+ ...(record.outcome === null
145
+ ? {}
146
+ : {
147
+ outcome: {
148
+ outcome: record.outcome.outcome,
149
+ ...(record.outcome.outcome === "ok"
150
+ ? { text: record.outcome.text }
151
+ : {}),
152
+ },
153
+ }),
154
+ ...(record.provenance === null
155
+ ? {}
156
+ : {
157
+ provenance: {
158
+ untrusted: record.provenance.untrusted,
159
+ audience: record.provenance.audience,
160
+ runnerOwner: toName(record.provenance.runnerOwner),
161
+ },
162
+ }),
163
+ };
164
+ },
165
+
166
+ runnerAvailability: async (input) => {
167
+ const availability = await app.runnerAvailability({
168
+ kind: input.kind,
169
+ owner: await ensureUser(input.owner),
170
+ ...(input.audience === undefined ? {} : { audience: input.audience }),
171
+ });
172
+ return {
173
+ available: availability.available,
174
+ ...(availability.reason === undefined
175
+ ? {}
176
+ : { reason: availability.reason }),
177
+ };
178
+ },
179
+
180
+ sweep: async () => {
181
+ await app.sweep();
182
+ },
183
+
184
+ // Owner ids here are `auth.users` uuids, not the names the checks use.
185
+ ownerId: (name) => ensureUser(name),
186
+
187
+ reset: async () => {
188
+ // Truncate rather than drop: the migration is what is under test, and
189
+ // re-running it between checks would be testing the migration instead.
190
+ // Each table names its own primary key — `byollm_job_cancels` is keyed by
191
+ // `job_id`, and PostgREST requires a filter on every delete.
192
+ const tables: readonly [string, string][] = [
193
+ ["byollm_job_cancels", "job_id"],
194
+ ["byollm_jobs", "id"],
195
+ ["byollm_pairings", "device_code_hash"],
196
+ ["byollm_runners", "id"],
197
+ ];
198
+ for (const [table, key] of tables) {
199
+ const { error } = await client.from(table).delete().not(key, "is", null);
200
+ if (error) throw new Error(`reset ${table}: ${error.message}`);
201
+ }
202
+ },
203
+ };
204
+
205
+ const report = await certify(target, {
206
+ onProgress: (result) => {
207
+ process.stdout.write(result.passed ? "." : "x");
208
+ },
209
+ });
210
+ process.stdout.write(`\n\n${formatReport(report)}`);
211
+ process.exit(report.passed ? 0 : 1);