@stardeck-customer-apps/testing 0.19.0 → 0.21.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.
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/test-app.ts
2
2
  import crypto8 from "crypto";
3
- import { readFileSync, existsSync } from "fs";
4
- import { resolve } from "path";
3
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
4
+ import { resolve as resolve2 } from "path";
5
5
 
6
6
  // src/pglite/database.ts
7
7
  import { PGlite } from "@electric-sql/pglite";
@@ -9,10 +9,10 @@ import { citext } from "@electric-sql/pglite/contrib/citext";
9
9
  import { pg_trgm } from "@electric-sql/pglite/contrib/pg_trgm";
10
10
  import { pgcrypto } from "@electric-sql/pglite/contrib/pgcrypto";
11
11
  import { uuid_ossp } from "@electric-sql/pglite/contrib/uuid_ossp";
12
- async function createPgliteDatabase() {
13
- const db = new PGlite({
14
- extensions: { pg_trgm, pgcrypto, uuid_ossp, citext }
15
- });
12
+ var EXTENSIONS = { pg_trgm, pgcrypto, uuid_ossp, citext };
13
+ var PGLITE_EXTENSION_NAMES = Object.keys(EXTENSIONS);
14
+ async function createPgliteDatabase(options = {}) {
15
+ const db = new PGlite({ extensions: EXTENSIONS, ...options });
16
16
  await db.waitReady;
17
17
  return db;
18
18
  }
@@ -33,10 +33,6 @@ async function listPublicTables(db) {
33
33
  `);
34
34
  return rows.map(({ name }) => name);
35
35
  }
36
- async function truncateAllTables(db) {
37
- const tables = await listPublicTables(db);
38
- if (tables.length) await db.exec(`TRUNCATE TABLE ${tables.join(", ")} RESTART IDENTITY`);
39
- }
40
36
  var identityParsers = null;
41
37
  function getIdentityParsers() {
42
38
  if (!identityParsers) {
@@ -49,6 +45,208 @@ function getIdentityParsers() {
49
45
  return identityParsers;
50
46
  }
51
47
 
48
+ // src/pglite/baseline.ts
49
+ var HARNESS_SCHEMA = "stardeck_test";
50
+ var SCHEMA_CHANGES = `${HARNESS_SCHEMA}.schema_changes`;
51
+ var SCHEMA_CHANGE_TRACKING_SQL = `
52
+ CREATE SCHEMA IF NOT EXISTS ${HARNESS_SCHEMA};
53
+ CREATE TABLE IF NOT EXISTS ${SCHEMA_CHANGES} ();
54
+ CREATE OR REPLACE FUNCTION ${HARNESS_SCHEMA}.mark_schema_changed() RETURNS event_trigger
55
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog
56
+ AS $$ BEGIN INSERT INTO ${SCHEMA_CHANGES} DEFAULT VALUES; END $$;
57
+ DROP EVENT TRIGGER IF EXISTS stardeck_test_ddl;
58
+ DROP EVENT TRIGGER IF EXISTS stardeck_test_drop;
59
+ CREATE EVENT TRIGGER stardeck_test_ddl ON ddl_command_end
60
+ EXECUTE FUNCTION ${HARNESS_SCHEMA}.mark_schema_changed();
61
+ CREATE EVENT TRIGGER stardeck_test_drop ON sql_drop
62
+ EXECUTE FUNCTION ${HARNESS_SCHEMA}.mark_schema_changed();
63
+ ALTER EVENT TRIGGER stardeck_test_ddl ENABLE ALWAYS;
64
+ ALTER EVENT TRIGGER stardeck_test_drop ENABLE ALWAYS;
65
+ `;
66
+ async function schemaChangedSinceBaseline(db) {
67
+ const { rows } = await db.query(
68
+ `SELECT EXISTS (SELECT FROM ${SCHEMA_CHANGES}) AS changed`
69
+ );
70
+ return rows[0]?.changed === true;
71
+ }
72
+ async function acceptSchemaChanges(db) {
73
+ await db.exec(`DELETE FROM ${SCHEMA_CHANGES};`);
74
+ }
75
+ async function sequenceStates(db) {
76
+ const { rows: sequences } = await db.query(`
77
+ SELECT format('%I.%I', n.nspname, c.relname) AS name
78
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
79
+ WHERE n.nspname = 'public' AND c.relkind = 'S'
80
+ `);
81
+ if (!sequences.length) return null;
82
+ const { rows } = await db.query(
83
+ sequences.map(
84
+ ({ name }) => `SELECT ${quoteLiteral(name)} AS name, last_value::text, is_called FROM ${name}`
85
+ ).join(" UNION ALL ")
86
+ );
87
+ return `SELECT ${rows.map((s) => `setval(${quoteLiteral(s.name)}, ${s.last_value}, ${s.is_called})`).join(", ")};`;
88
+ }
89
+ function quoteLiteral(value) {
90
+ return `'${value.replaceAll("'", "''")}'`;
91
+ }
92
+ async function captureBaseline(db) {
93
+ const { rows: unsilenceable } = await db.query(`
94
+ SELECT FROM pg_trigger t JOIN pg_class c ON c.oid = t.tgrelid
95
+ WHERE c.relnamespace = 'public'::regnamespace AND NOT t.tgisinternal
96
+ AND t.tgenabled IN ('A', 'R')`);
97
+ if (unsilenceable.length) return null;
98
+ const tables = await listPublicTables(db);
99
+ const statements = [];
100
+ await db.exec(`DROP SCHEMA IF EXISTS ${HARNESS_SCHEMA}_baseline CASCADE;
101
+ CREATE SCHEMA ${HARNESS_SCHEMA}_baseline;`);
102
+ if (tables.length) {
103
+ const { rows: populated } = await db.query(
104
+ tables.map(
105
+ (table, index) => `SELECT ${index} AS index, (
106
+ SELECT string_agg(quote_ident(attname), ', ' ORDER BY attnum) FROM pg_attribute
107
+ WHERE attrelid = ${quoteLiteral(table)}::regclass AND attnum > 0
108
+ AND NOT attisdropped AND attgenerated = ''
109
+ ) AS columns WHERE EXISTS (SELECT 1 FROM ONLY ${table})`
110
+ ).join(" UNION ALL ")
111
+ );
112
+ for (const { index, columns } of populated) {
113
+ const copy = `${HARNESS_SCHEMA}_baseline.t${index}`;
114
+ await db.exec(`CREATE TABLE ${copy} AS SELECT ${columns} FROM ONLY ${tables[index]};`);
115
+ statements.push(
116
+ `INSERT INTO ${tables[index]} (${columns}) OVERRIDING SYSTEM VALUE SELECT ${columns} FROM ${copy};`
117
+ );
118
+ }
119
+ }
120
+ const setvals = await sequenceStates(db);
121
+ if (setvals) statements.push(setvals);
122
+ return { restoreSql: statements.join("\n") };
123
+ }
124
+ async function restoreBaseline(db, baseline) {
125
+ const tables = await listPublicTables(db);
126
+ const truncate = tables.length ? `TRUNCATE TABLE ${tables.join(", ")} RESTART IDENTITY;` : "";
127
+ try {
128
+ await db.exec(`BEGIN; SET LOCAL session_replication_role = replica;
129
+ ${truncate}
130
+ SELECT setval(format('%I.%I', schemaname, sequencename), start_value, false)
131
+ FROM pg_sequences WHERE schemaname = 'public';
132
+ ${baseline.restoreSql}
133
+ COMMIT;`);
134
+ } catch (error) {
135
+ await db.exec("ROLLBACK;").catch(() => {
136
+ });
137
+ throw error;
138
+ }
139
+ }
140
+
141
+ // src/pglite/snapshot.ts
142
+ import { createHash } from "crypto";
143
+ import {
144
+ existsSync,
145
+ mkdirSync,
146
+ readdirSync,
147
+ readFileSync,
148
+ renameSync,
149
+ rmSync,
150
+ statSync,
151
+ utimesSync,
152
+ writeFileSync
153
+ } from "fs";
154
+ import { dirname, join, resolve } from "path";
155
+ var SNAPSHOT_FORMAT = 2;
156
+ var MAX_SNAPSHOTS = 8;
157
+ function snapshotKey(schemaSql) {
158
+ const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
159
+ return createHash("sha256").update(JSON.stringify({ format: SNAPSHOT_FORMAT, extensions: PGLITE_EXTENSION_NAMES, day })).update(SCHEMA_CHANGE_TRACKING_SQL).update(schemaSql).digest("hex").slice(0, 32);
160
+ }
161
+ function snapshotPath(schemaSql) {
162
+ return resolve(
163
+ process.cwd(),
164
+ "node_modules/.cache/stardeck-testing",
165
+ `${snapshotKey(schemaSql)}.tar`
166
+ );
167
+ }
168
+ var CUSTOM_SETTING = /\bset_config\s*\(\s*(?:'([^']*)'|(\$\w*\$)([\s\S]*?)\2)|\bSET\s+(?:SESSION\s+)?(?:"([^"]*)"|([a-z_][\w$]*\.[\w$.]+))/gi;
169
+ async function sessionSettings(db, schemaSql) {
170
+ const custom = [...schemaSql.matchAll(CUSTOM_SETTING)].map(
171
+ ([, quoted, , dollarQuoted, identifier, bare]) => quoted ?? dollarQuoted ?? identifier ?? bare
172
+ ).filter((name) => name?.includes("."));
173
+ const { rows } = await db.query(
174
+ `SELECT string_agg(name || '=' || setting, ',' ORDER BY name) AS settings FROM (
175
+ SELECT name, setting FROM pg_settings WHERE source = 'session'
176
+ UNION ALL
177
+ SELECT name, current_setting(name, true) FROM unnest($1::text[]) AS name
178
+ WHERE current_setting(name, true) IS NOT NULL
179
+ ) settings`,
180
+ [custom]
181
+ );
182
+ return rows[0]?.settings ?? "";
183
+ }
184
+ async function buildSchemaDatabase(schemaSql) {
185
+ const db = await createPgliteDatabase();
186
+ try {
187
+ await db.exec(SCHEMA_CHANGE_TRACKING_SQL);
188
+ const settingsBefore = await sessionSettings(db, schemaSql);
189
+ await applySchema(db, schemaSql);
190
+ const { rows: sessionBound } = await db.query(
191
+ `SELECT FROM pg_class WHERE relpersistence IN ('u', 't')`
192
+ );
193
+ const cacheable = !sessionBound.length && await sessionSettings(db, schemaSql) === settingsBefore;
194
+ await db.exec(`COMMENT ON SCHEMA stardeck_test IS '${snapshotKey(schemaSql)}'`);
195
+ await db.exec("CHECKPOINT");
196
+ return { db, cacheable };
197
+ } catch (error) {
198
+ await db.close().catch(() => {
199
+ });
200
+ throw error;
201
+ }
202
+ }
203
+ function pruneSnapshots(dir) {
204
+ const snapshots = readdirSync(dir).filter((name) => name.endsWith(".tar")).map((name) => ({ path: join(dir, name), mtime: statSync(join(dir, name)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
205
+ for (const { path } of snapshots.slice(MAX_SNAPSHOTS)) rmSync(path, { force: true });
206
+ }
207
+ async function writeSnapshot(path, db) {
208
+ try {
209
+ const data = Buffer.from(await (await db.dumpDataDir("none")).arrayBuffer());
210
+ mkdirSync(dirname(path), { recursive: true });
211
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
212
+ writeFileSync(tmp, data);
213
+ renameSync(tmp, path);
214
+ pruneSnapshots(dirname(path));
215
+ } catch (error) {
216
+ console.warn(`[stardeck-testing] could not cache schema snapshot at ${path}:`, error);
217
+ }
218
+ }
219
+ async function loadSnapshot(path, key) {
220
+ let db = null;
221
+ try {
222
+ db = await createPgliteDatabase({ loadDataDir: new Blob([readFileSync(path)]) });
223
+ const { rows } = await db.query(
224
+ `SELECT obj_description(to_regnamespace('stardeck_test'), 'pg_namespace') AS key`
225
+ );
226
+ if (rows[0]?.key === key) {
227
+ const now3 = /* @__PURE__ */ new Date();
228
+ utimesSync(path, now3, now3);
229
+ return db;
230
+ }
231
+ console.warn(`[stardeck-testing] rebuilding schema snapshot ${path}: key mismatch`);
232
+ } catch (error) {
233
+ console.warn(`[stardeck-testing] rebuilding unreadable schema snapshot ${path}:`, error);
234
+ }
235
+ await db?.close().catch(() => {
236
+ });
237
+ return null;
238
+ }
239
+ async function bootWithSchema(schemaSql) {
240
+ const path = snapshotPath(schemaSql);
241
+ if (existsSync(path)) {
242
+ const loaded = await loadSnapshot(path, snapshotKey(schemaSql));
243
+ if (loaded) return loaded;
244
+ }
245
+ const { db, cacheable } = await buildSchemaDatabase(schemaSql);
246
+ if (cacheable) await writeSnapshot(path, db);
247
+ return db;
248
+ }
249
+
52
250
  // src/global-singleton.ts
53
251
  function globalSingleton(key, create) {
54
252
  const symbol = /* @__PURE__ */ Symbol.for(`stardeck-testing.${key}`);
@@ -2865,6 +3063,7 @@ function createEdge() {
2865
3063
  }
2866
3064
 
2867
3065
  // src/simulator/starlens.ts
3066
+ var LATEST_VERSION = "v2";
2868
3067
  function starlensFailure(error, status, code) {
2869
3068
  return json({ success: false, error, code }, status);
2870
3069
  }
@@ -2883,11 +3082,18 @@ function nullFlagged(output, issues) {
2883
3082
  }
2884
3083
  return copy;
2885
3084
  }
2886
- async function handleStarlensExtract(request, signedDeploymentId) {
2887
- const body = await readJsonBody(request);
2888
- if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2889
- return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2890
- }
3085
+ function isInputShape(input) {
3086
+ if (!input || typeof input !== "object") return false;
3087
+ const i = input;
3088
+ return typeof i.url === "string" || typeof i.data === "string" && i.data.length > 0 && typeof i.mediaType === "string";
3089
+ }
3090
+ var NON_IMAGE_EXTENSION = /\.(pdf|mp3|m4a|wav|ogg|aac|flac|mp4|mov|webm|mkv)(\?|#|$)/i;
3091
+ function isCroppable(input) {
3092
+ if (typeof input.mediaType === "string") return input.mediaType.startsWith("image/");
3093
+ const name = typeof input.filename === "string" ? input.filename : input.url;
3094
+ return !(typeof name === "string" && NON_IMAGE_EXTENSION.test(name));
3095
+ }
3096
+ function validateStarlensBody(body) {
2891
3097
  const inputs = Array.isArray(body.inputs) ? body.inputs : [];
2892
3098
  if (inputs.length === 0) {
2893
3099
  return starlensFailure(
@@ -2896,6 +3102,13 @@ async function handleStarlensExtract(request, signedDeploymentId) {
2896
3102
  "VALIDATION_ERROR"
2897
3103
  );
2898
3104
  }
3105
+ if (!inputs.every(isInputShape)) {
3106
+ return starlensFailure(
3107
+ "Validation failed \u2014 inputs: each input needs a url, or data with a mediaType",
3108
+ 400,
3109
+ "VALIDATION_ERROR"
3110
+ );
3111
+ }
2899
3112
  const schema = body.schema;
2900
3113
  if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
2901
3114
  return starlensFailure(
@@ -2904,8 +3117,35 @@ async function handleStarlensExtract(request, signedDeploymentId) {
2904
3117
  "VALIDATION_ERROR"
2905
3118
  );
2906
3119
  }
3120
+ const version = body.version ?? "latest";
3121
+ if (version !== "v1" && version !== "v2" && version !== "latest") {
3122
+ return starlensFailure("Validation failed \u2014 version: unknown version", 400, "VALIDATION_ERROR");
3123
+ }
3124
+ const effort = body.effort ?? "low";
3125
+ if (effort !== "low" && effort !== "high") {
3126
+ return starlensFailure("Validation failed \u2014 effort: low or high", 400, "VALIDATION_ERROR");
3127
+ }
3128
+ const resolved = version === "latest" ? LATEST_VERSION : version;
3129
+ if (effort === "high" && resolved === "v1") {
3130
+ return starlensFailure(
3131
+ `Starlens v1 does not support effort "high"; use version "v2"`,
3132
+ 400,
3133
+ "unsupported_effort"
3134
+ );
3135
+ }
3136
+ return { version, resolved, effort, inputs, schema };
3137
+ }
3138
+ async function handleStarlensExtract(request, signedDeploymentId) {
3139
+ const body = await readJsonBody(request);
3140
+ const valid = validateStarlensBody(body);
3141
+ if (valid instanceof Response) return valid;
3142
+ const { version, resolved, effort, inputs, schema } = valid;
3143
+ if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
3144
+ return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
3145
+ }
2907
3146
  const call = {
2908
- version: typeof body.version === "string" ? body.version : "latest",
3147
+ version,
3148
+ effort,
2909
3149
  inputs: inputs.map((i) => ({
2910
3150
  name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
2911
3151
  mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
@@ -2927,8 +3167,10 @@ async function handleStarlensExtract(request, signedDeploymentId) {
2927
3167
  return starlensFailure(reply.reject, 422, "unsupported_document");
2928
3168
  }
2929
3169
  const issues = reply.issues ?? [];
3170
+ const ran = reply.effort ?? (effort === "high" && !inputs.some(isCroppable) ? "low" : effort);
2930
3171
  return success({
2931
- version: call.version === "latest" ? "v1" : call.version,
3172
+ version: resolved,
3173
+ effort: ran,
2932
3174
  output: nullFlagged(reply.output, issues),
2933
3175
  issues,
2934
3176
  usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
@@ -3198,8 +3440,8 @@ function buildUser(partial) {
3198
3440
  function loadSchemaSql(options) {
3199
3441
  if (options.schemaSql !== void 0) return options.schemaSql;
3200
3442
  if (options.schema === null) return null;
3201
- const schemaPath = resolve(process.cwd(), options.schema ?? DEFAULT_SCHEMA_PATH);
3202
- if (!existsSync(schemaPath)) {
3443
+ const schemaPath = resolve2(process.cwd(), options.schema ?? DEFAULT_SCHEMA_PATH);
3444
+ if (!existsSync2(schemaPath)) {
3203
3445
  if (options.schema) {
3204
3446
  throw new Error(
3205
3447
  `[stardeck-testing] Schema snapshot not found at ${schemaPath}. Run \`npx stardeck-data-store generate-types\` to regenerate it.`
@@ -3207,7 +3449,7 @@ function loadSchemaSql(options) {
3207
3449
  }
3208
3450
  return null;
3209
3451
  }
3210
- return readFileSync(schemaPath, "utf-8");
3452
+ return readFileSync2(schemaPath, "utf-8");
3211
3453
  }
3212
3454
  async function createTestApp(options = {}) {
3213
3455
  if (state.db) {
@@ -3218,15 +3460,23 @@ async function createTestApp(options = {}) {
3218
3460
  applyTestEnv(options);
3219
3461
  state.allowNetwork = options.allowNetwork ?? false;
3220
3462
  installFetchRouter();
3221
- const db = await createPgliteDatabase();
3463
+ const schemaSql = loadSchemaSql(options) ?? "";
3464
+ const db = await bootWithSchema(schemaSql);
3222
3465
  state.db = db;
3223
- const initialize = async () => {
3224
- const schemaSql = loadSchemaSql(options);
3225
- if (schemaSql) await applySchema(db, schemaSql);
3466
+ let baseline;
3467
+ const seed = async () => {
3226
3468
  if (options.seed) await options.seed(db);
3469
+ await acceptSchemaChanges(db);
3470
+ };
3471
+ const rebuild = async () => {
3472
+ await dropAllTables(db);
3473
+ await applySchema(db, schemaSql);
3474
+ baseline = await captureBaseline(db);
3475
+ await seed();
3227
3476
  };
3228
3477
  try {
3229
- await initialize();
3478
+ baseline = await captureBaseline(db);
3479
+ await seed();
3230
3480
  } catch (error) {
3231
3481
  state.db = null;
3232
3482
  await db.close().catch(() => {
@@ -3272,12 +3522,15 @@ async function createTestApp(options = {}) {
3272
3522
  return tokens;
3273
3523
  },
3274
3524
  async reset(resetOptions) {
3275
- if (resetOptions?.mode === "data") {
3276
- await truncateAllTables(db);
3277
- if (options.seed) await options.seed(db);
3525
+ if (resetOptions?.mode === "schema" || !baseline || await schemaChangedSinceBaseline(db)) {
3526
+ await rebuild();
3278
3527
  } else {
3279
- await dropAllTables(db);
3280
- await initialize();
3528
+ await restoreBaseline(db, baseline);
3529
+ try {
3530
+ await seed();
3531
+ } catch {
3532
+ await rebuild();
3533
+ }
3281
3534
  }
3282
3535
  state.currentUser = null;
3283
3536
  state.sessions.clear();
package/dist/setup.js CHANGED
@@ -504,6 +504,8 @@ var import_citext = require("@electric-sql/pglite/contrib/citext");
504
504
  var import_pg_trgm = require("@electric-sql/pglite/contrib/pg_trgm");
505
505
  var import_pgcrypto = require("@electric-sql/pglite/contrib/pgcrypto");
506
506
  var import_uuid_ossp = require("@electric-sql/pglite/contrib/uuid_ossp");
507
+ var EXTENSIONS = { pg_trgm: import_pg_trgm.pg_trgm, pgcrypto: import_pgcrypto.pgcrypto, uuid_ossp: import_uuid_ossp.uuid_ossp, citext: import_citext.citext };
508
+ var PGLITE_EXTENSION_NAMES = Object.keys(EXTENSIONS);
507
509
  var identityParsers = null;
508
510
  function getIdentityParsers() {
509
511
  if (!identityParsers) {
@@ -2245,6 +2247,7 @@ async function handleEdgeRequest(request, subPath) {
2245
2247
  }
2246
2248
 
2247
2249
  // src/simulator/starlens.ts
2250
+ var LATEST_VERSION = "v2";
2248
2251
  function starlensFailure(error, status, code) {
2249
2252
  return json({ success: false, error, code }, status);
2250
2253
  }
@@ -2263,11 +2266,18 @@ function nullFlagged(output, issues) {
2263
2266
  }
2264
2267
  return copy;
2265
2268
  }
2266
- async function handleStarlensExtract(request, signedDeploymentId) {
2267
- const body = await readJsonBody(request);
2268
- if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2269
- return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2270
- }
2269
+ function isInputShape(input) {
2270
+ if (!input || typeof input !== "object") return false;
2271
+ const i = input;
2272
+ return typeof i.url === "string" || typeof i.data === "string" && i.data.length > 0 && typeof i.mediaType === "string";
2273
+ }
2274
+ var NON_IMAGE_EXTENSION = /\.(pdf|mp3|m4a|wav|ogg|aac|flac|mp4|mov|webm|mkv)(\?|#|$)/i;
2275
+ function isCroppable(input) {
2276
+ if (typeof input.mediaType === "string") return input.mediaType.startsWith("image/");
2277
+ const name = typeof input.filename === "string" ? input.filename : input.url;
2278
+ return !(typeof name === "string" && NON_IMAGE_EXTENSION.test(name));
2279
+ }
2280
+ function validateStarlensBody(body) {
2271
2281
  const inputs = Array.isArray(body.inputs) ? body.inputs : [];
2272
2282
  if (inputs.length === 0) {
2273
2283
  return starlensFailure(
@@ -2276,6 +2286,13 @@ async function handleStarlensExtract(request, signedDeploymentId) {
2276
2286
  "VALIDATION_ERROR"
2277
2287
  );
2278
2288
  }
2289
+ if (!inputs.every(isInputShape)) {
2290
+ return starlensFailure(
2291
+ "Validation failed \u2014 inputs: each input needs a url, or data with a mediaType",
2292
+ 400,
2293
+ "VALIDATION_ERROR"
2294
+ );
2295
+ }
2279
2296
  const schema = body.schema;
2280
2297
  if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
2281
2298
  return starlensFailure(
@@ -2284,8 +2301,35 @@ async function handleStarlensExtract(request, signedDeploymentId) {
2284
2301
  "VALIDATION_ERROR"
2285
2302
  );
2286
2303
  }
2304
+ const version = body.version ?? "latest";
2305
+ if (version !== "v1" && version !== "v2" && version !== "latest") {
2306
+ return starlensFailure("Validation failed \u2014 version: unknown version", 400, "VALIDATION_ERROR");
2307
+ }
2308
+ const effort = body.effort ?? "low";
2309
+ if (effort !== "low" && effort !== "high") {
2310
+ return starlensFailure("Validation failed \u2014 effort: low or high", 400, "VALIDATION_ERROR");
2311
+ }
2312
+ const resolved = version === "latest" ? LATEST_VERSION : version;
2313
+ if (effort === "high" && resolved === "v1") {
2314
+ return starlensFailure(
2315
+ `Starlens v1 does not support effort "high"; use version "v2"`,
2316
+ 400,
2317
+ "unsupported_effort"
2318
+ );
2319
+ }
2320
+ return { version, resolved, effort, inputs, schema };
2321
+ }
2322
+ async function handleStarlensExtract(request, signedDeploymentId) {
2323
+ const body = await readJsonBody(request);
2324
+ const valid = validateStarlensBody(body);
2325
+ if (valid instanceof Response) return valid;
2326
+ const { version, resolved, effort, inputs, schema } = valid;
2327
+ if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2328
+ return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2329
+ }
2287
2330
  const call = {
2288
- version: typeof body.version === "string" ? body.version : "latest",
2331
+ version,
2332
+ effort,
2289
2333
  inputs: inputs.map((i) => ({
2290
2334
  name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
2291
2335
  mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
@@ -2307,8 +2351,10 @@ async function handleStarlensExtract(request, signedDeploymentId) {
2307
2351
  return starlensFailure(reply.reject, 422, "unsupported_document");
2308
2352
  }
2309
2353
  const issues = reply.issues ?? [];
2354
+ const ran = reply.effort ?? (effort === "high" && !inputs.some(isCroppable) ? "low" : effort);
2310
2355
  return success({
2311
- version: call.version === "latest" ? "v1" : call.version,
2356
+ version: resolved,
2357
+ effort: ran,
2312
2358
  output: nullFlagged(reply.output, issues),
2313
2359
  issues,
2314
2360
  usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
package/dist/setup.mjs CHANGED
@@ -480,6 +480,8 @@ import { citext } from "@electric-sql/pglite/contrib/citext";
480
480
  import { pg_trgm } from "@electric-sql/pglite/contrib/pg_trgm";
481
481
  import { pgcrypto } from "@electric-sql/pglite/contrib/pgcrypto";
482
482
  import { uuid_ossp } from "@electric-sql/pglite/contrib/uuid_ossp";
483
+ var EXTENSIONS = { pg_trgm, pgcrypto, uuid_ossp, citext };
484
+ var PGLITE_EXTENSION_NAMES = Object.keys(EXTENSIONS);
483
485
  var identityParsers = null;
484
486
  function getIdentityParsers() {
485
487
  if (!identityParsers) {
@@ -2221,6 +2223,7 @@ async function handleEdgeRequest(request, subPath) {
2221
2223
  }
2222
2224
 
2223
2225
  // src/simulator/starlens.ts
2226
+ var LATEST_VERSION = "v2";
2224
2227
  function starlensFailure(error, status, code) {
2225
2228
  return json({ success: false, error, code }, status);
2226
2229
  }
@@ -2239,11 +2242,18 @@ function nullFlagged(output, issues) {
2239
2242
  }
2240
2243
  return copy;
2241
2244
  }
2242
- async function handleStarlensExtract(request, signedDeploymentId) {
2243
- const body = await readJsonBody(request);
2244
- if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2245
- return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2246
- }
2245
+ function isInputShape(input) {
2246
+ if (!input || typeof input !== "object") return false;
2247
+ const i = input;
2248
+ return typeof i.url === "string" || typeof i.data === "string" && i.data.length > 0 && typeof i.mediaType === "string";
2249
+ }
2250
+ var NON_IMAGE_EXTENSION = /\.(pdf|mp3|m4a|wav|ogg|aac|flac|mp4|mov|webm|mkv)(\?|#|$)/i;
2251
+ function isCroppable(input) {
2252
+ if (typeof input.mediaType === "string") return input.mediaType.startsWith("image/");
2253
+ const name = typeof input.filename === "string" ? input.filename : input.url;
2254
+ return !(typeof name === "string" && NON_IMAGE_EXTENSION.test(name));
2255
+ }
2256
+ function validateStarlensBody(body) {
2247
2257
  const inputs = Array.isArray(body.inputs) ? body.inputs : [];
2248
2258
  if (inputs.length === 0) {
2249
2259
  return starlensFailure(
@@ -2252,6 +2262,13 @@ async function handleStarlensExtract(request, signedDeploymentId) {
2252
2262
  "VALIDATION_ERROR"
2253
2263
  );
2254
2264
  }
2265
+ if (!inputs.every(isInputShape)) {
2266
+ return starlensFailure(
2267
+ "Validation failed \u2014 inputs: each input needs a url, or data with a mediaType",
2268
+ 400,
2269
+ "VALIDATION_ERROR"
2270
+ );
2271
+ }
2255
2272
  const schema = body.schema;
2256
2273
  if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
2257
2274
  return starlensFailure(
@@ -2260,8 +2277,35 @@ async function handleStarlensExtract(request, signedDeploymentId) {
2260
2277
  "VALIDATION_ERROR"
2261
2278
  );
2262
2279
  }
2280
+ const version = body.version ?? "latest";
2281
+ if (version !== "v1" && version !== "v2" && version !== "latest") {
2282
+ return starlensFailure("Validation failed \u2014 version: unknown version", 400, "VALIDATION_ERROR");
2283
+ }
2284
+ const effort = body.effort ?? "low";
2285
+ if (effort !== "low" && effort !== "high") {
2286
+ return starlensFailure("Validation failed \u2014 effort: low or high", 400, "VALIDATION_ERROR");
2287
+ }
2288
+ const resolved = version === "latest" ? LATEST_VERSION : version;
2289
+ if (effort === "high" && resolved === "v1") {
2290
+ return starlensFailure(
2291
+ `Starlens v1 does not support effort "high"; use version "v2"`,
2292
+ 400,
2293
+ "unsupported_effort"
2294
+ );
2295
+ }
2296
+ return { version, resolved, effort, inputs, schema };
2297
+ }
2298
+ async function handleStarlensExtract(request, signedDeploymentId) {
2299
+ const body = await readJsonBody(request);
2300
+ const valid = validateStarlensBody(body);
2301
+ if (valid instanceof Response) return valid;
2302
+ const { version, resolved, effort, inputs, schema } = valid;
2303
+ if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2304
+ return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2305
+ }
2263
2306
  const call = {
2264
- version: typeof body.version === "string" ? body.version : "latest",
2307
+ version,
2308
+ effort,
2265
2309
  inputs: inputs.map((i) => ({
2266
2310
  name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
2267
2311
  mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
@@ -2283,8 +2327,10 @@ async function handleStarlensExtract(request, signedDeploymentId) {
2283
2327
  return starlensFailure(reply.reject, 422, "unsupported_document");
2284
2328
  }
2285
2329
  const issues = reply.issues ?? [];
2330
+ const ran = reply.effort ?? (effort === "high" && !inputs.some(isCroppable) ? "low" : effort);
2286
2331
  return success({
2287
- version: call.version === "latest" ? "v1" : call.version,
2332
+ version: resolved,
2333
+ effort: ran,
2288
2334
  output: nullFlagged(reply.output, issues),
2289
2335
  issues,
2290
2336
  usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/testing",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "Vitest test harness for Stardeck customer apps — in-process Postgres (PGlite) plus a control-plane simulator so the real Stardeck SDKs run unmodified in tests",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",