@stardeck-customer-apps/testing 0.14.0 → 0.15.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/SKILL.md CHANGED
@@ -451,6 +451,14 @@ cursor, limit })`; `list()` intentionally keeps its previous array shape.
451
451
  display, test print, `getCapabilities()`) — captured in `app.edge`; seed
452
452
  devices/peripherals for pairing pickers via `app.edge.seedDevices()` /
453
453
  `seedPeripherals()`.
454
+ - `createStarlensClient().extract()` from the starlens SDK — there is no model in
455
+ tests: register what Starlens answers with `app.starlens.mock({ output, issues })`
456
+ (a successful extraction; each issue path comes back `null` in `output`, as in
457
+ production) or `app.starlens.reject("not a receipt")` (the 422 the SDK maps to
458
+ `UnsupportedDocumentError`). An unregistered call fails with a descriptive 502 so a
459
+ test never passes on an invented extraction. `app.starlens.calls` records each
460
+ request's version, input names/media types (never bytes), schema and
461
+ instructions — assert your `.describe()` aliases reached the wire there.
454
462
 
455
463
  ### Receipt layout and printer capabilities
456
464
 
package/dist/index.d.mts CHANGED
@@ -462,6 +462,44 @@ interface TestEdge {
462
462
  clear(): void;
463
463
  get count(): number;
464
464
  }
465
+ /** One Starlens request the app made, as the simulator saw it (names only, never bytes). */
466
+ interface CapturedStarlensCall {
467
+ version: string;
468
+ inputs: {
469
+ name: string;
470
+ mediaType: string | null;
471
+ source: "url" | "data";
472
+ }[];
473
+ schema: Record<string, unknown>;
474
+ instructions?: string;
475
+ }
476
+ /**
477
+ * What the simulated Starlens answers next. `{ output, issues }` is returned as a
478
+ * successful extraction with every issue path nulled in `output`, exactly as the control
479
+ * plane does; `{ reject }` becomes the 422 `unsupported_document` refusal the SDK maps to
480
+ * `UnsupportedDocumentError`.
481
+ */
482
+ type StarlensReply = {
483
+ output: unknown;
484
+ issues?: {
485
+ path: string;
486
+ reason: string;
487
+ }[];
488
+ } | {
489
+ reject: string;
490
+ };
491
+ interface TestStarlens {
492
+ /** Queue the reply for the next `extract()` call. */
493
+ mock(reply: StarlensReply): void;
494
+ /** Queue a refusal for the next `extract()` call. */
495
+ reject(reason: string): void;
496
+ /** Reply used whenever the queue is empty (null = fail with a descriptive 502). */
497
+ mockDefault(reply: StarlensReply | null): void;
498
+ get calls(): CapturedStarlensCall[];
499
+ latestCall(): CapturedStarlensCall | undefined;
500
+ clear(): void;
501
+ get count(): number;
502
+ }
465
503
  interface TestAppOptions {
466
504
  /** Data stores exposed through the production-shaped STARDECK_DATA_STORES manifest. */
467
505
  dataStores?: TestDataStoreInput[];
@@ -506,6 +544,8 @@ interface TestApp {
506
544
  messages: TestMessages;
507
545
  /** Captured edge print/display ops and peripheral bindings from edge-sdk. */
508
546
  edge: TestEdge;
547
+ /** Scripted replies and captured calls for the Starlens SDK. */
548
+ starlens: TestStarlens;
509
549
  /** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
510
550
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
511
551
  /**
@@ -571,4 +611,4 @@ declare const TEST_ENV_DEFAULTS: {
571
611
  /** Default location of the DDL snapshot written by `generate-types`. */
572
612
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
573
613
 
574
- export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
614
+ export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
package/dist/index.d.ts CHANGED
@@ -462,6 +462,44 @@ interface TestEdge {
462
462
  clear(): void;
463
463
  get count(): number;
464
464
  }
465
+ /** One Starlens request the app made, as the simulator saw it (names only, never bytes). */
466
+ interface CapturedStarlensCall {
467
+ version: string;
468
+ inputs: {
469
+ name: string;
470
+ mediaType: string | null;
471
+ source: "url" | "data";
472
+ }[];
473
+ schema: Record<string, unknown>;
474
+ instructions?: string;
475
+ }
476
+ /**
477
+ * What the simulated Starlens answers next. `{ output, issues }` is returned as a
478
+ * successful extraction with every issue path nulled in `output`, exactly as the control
479
+ * plane does; `{ reject }` becomes the 422 `unsupported_document` refusal the SDK maps to
480
+ * `UnsupportedDocumentError`.
481
+ */
482
+ type StarlensReply = {
483
+ output: unknown;
484
+ issues?: {
485
+ path: string;
486
+ reason: string;
487
+ }[];
488
+ } | {
489
+ reject: string;
490
+ };
491
+ interface TestStarlens {
492
+ /** Queue the reply for the next `extract()` call. */
493
+ mock(reply: StarlensReply): void;
494
+ /** Queue a refusal for the next `extract()` call. */
495
+ reject(reason: string): void;
496
+ /** Reply used whenever the queue is empty (null = fail with a descriptive 502). */
497
+ mockDefault(reply: StarlensReply | null): void;
498
+ get calls(): CapturedStarlensCall[];
499
+ latestCall(): CapturedStarlensCall | undefined;
500
+ clear(): void;
501
+ get count(): number;
502
+ }
465
503
  interface TestAppOptions {
466
504
  /** Data stores exposed through the production-shaped STARDECK_DATA_STORES manifest. */
467
505
  dataStores?: TestDataStoreInput[];
@@ -506,6 +544,8 @@ interface TestApp {
506
544
  messages: TestMessages;
507
545
  /** Captured edge print/display ops and peripheral bindings from edge-sdk. */
508
546
  edge: TestEdge;
547
+ /** Scripted replies and captured calls for the Starlens SDK. */
548
+ starlens: TestStarlens;
509
549
  /** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
510
550
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
511
551
  /**
@@ -571,4 +611,4 @@ declare const TEST_ENV_DEFAULTS: {
571
611
  /** Default location of the DDL snapshot written by `generate-types`. */
572
612
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
573
613
 
574
- export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
614
+ export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
package/dist/index.js CHANGED
@@ -144,6 +144,9 @@ var state = globalSingleton("state", () => ({
144
144
  edgeBindings: /* @__PURE__ */ new Map(),
145
145
  edgeCapabilities: null,
146
146
  edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
147
+ starlensCalls: [],
148
+ starlensReplies: [],
149
+ starlensDefaultReply: null,
147
150
  allowNetwork: false
148
151
  }));
149
152
  function requireDb() {
@@ -2553,6 +2556,104 @@ function createEdge() {
2553
2556
  };
2554
2557
  }
2555
2558
 
2559
+ // src/simulator/starlens.ts
2560
+ function starlensFailure(error, status, code) {
2561
+ return json({ success: false, error, code }, status);
2562
+ }
2563
+ function nullFlagged(output, issues) {
2564
+ const copy = JSON.parse(JSON.stringify(output ?? null));
2565
+ for (const { path } of issues) {
2566
+ const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
2567
+ const last = keys.pop();
2568
+ const parent = keys.reduce(
2569
+ (o, k) => o != null && typeof o === "object" ? o[k] : void 0,
2570
+ copy
2571
+ );
2572
+ if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
2573
+ parent[last] = null;
2574
+ }
2575
+ }
2576
+ return copy;
2577
+ }
2578
+ async function handleStarlensExtract(request, signedDeploymentId) {
2579
+ const body = await readJsonBody(request);
2580
+ if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2581
+ return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2582
+ }
2583
+ const inputs = Array.isArray(body.inputs) ? body.inputs : [];
2584
+ if (inputs.length === 0) {
2585
+ return starlensFailure(
2586
+ "Validation failed \u2014 inputs: at least one input",
2587
+ 400,
2588
+ "VALIDATION_ERROR"
2589
+ );
2590
+ }
2591
+ const schema = body.schema;
2592
+ if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
2593
+ return starlensFailure(
2594
+ "Validation failed \u2014 schema.properties is required",
2595
+ 400,
2596
+ "VALIDATION_ERROR"
2597
+ );
2598
+ }
2599
+ const call = {
2600
+ version: typeof body.version === "string" ? body.version : "latest",
2601
+ inputs: inputs.map((i) => ({
2602
+ name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
2603
+ mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
2604
+ source: typeof i.url === "string" ? "url" : "data"
2605
+ })),
2606
+ schema,
2607
+ instructions: typeof body.instructions === "string" ? body.instructions : void 0
2608
+ };
2609
+ state.starlensCalls.push(call);
2610
+ const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
2611
+ if (!reply) {
2612
+ return starlensFailure(
2613
+ "[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
2614
+ 502,
2615
+ "model_failed"
2616
+ );
2617
+ }
2618
+ if ("reject" in reply) {
2619
+ return starlensFailure(reply.reject, 422, "unsupported_document");
2620
+ }
2621
+ const issues = reply.issues ?? [];
2622
+ return success({
2623
+ version: call.version === "latest" ? "v1" : call.version,
2624
+ output: nullFlagged(reply.output, issues),
2625
+ issues,
2626
+ usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
2627
+ });
2628
+ }
2629
+ function createStarlens() {
2630
+ return {
2631
+ mock(reply) {
2632
+ state.starlensReplies.push(reply);
2633
+ },
2634
+ reject(reason) {
2635
+ state.starlensReplies.push({ reject: reason });
2636
+ },
2637
+ mockDefault(reply) {
2638
+ state.starlensDefaultReply = reply;
2639
+ },
2640
+ get calls() {
2641
+ return state.starlensCalls;
2642
+ },
2643
+ latestCall() {
2644
+ return state.starlensCalls[state.starlensCalls.length - 1];
2645
+ },
2646
+ clear() {
2647
+ state.starlensCalls = [];
2648
+ state.starlensReplies = [];
2649
+ state.starlensDefaultReply = null;
2650
+ },
2651
+ get count() {
2652
+ return state.starlensCalls.length;
2653
+ }
2654
+ };
2655
+ }
2656
+
2556
2657
  // src/simulator/router.ts
2557
2658
  var fetchHolder = globalSingleton("fetch-holder", () => ({
2558
2659
  originalFetch: null
@@ -2574,7 +2675,8 @@ function requiresDeploymentHmac(request, url) {
2574
2675
  /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
2575
2676
  );
2576
2677
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2577
- return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2678
+ const isStarlens = url.pathname === "/api/starlens/extract";
2679
+ return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2578
2680
  }
2579
2681
  function deploymentIdFromPath(url) {
2580
2682
  const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
@@ -2604,6 +2706,7 @@ async function handleSimulatedRequest(request, url) {
2604
2706
  );
2605
2707
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2606
2708
  const isStorageHost = url.hostname === STORAGE_TEST_HOST;
2709
+ let signedDeploymentId = null;
2607
2710
  if (requiresDeploymentHmac(request, url)) {
2608
2711
  const authHeader = request.headers.get("X-Stardeck-Auth");
2609
2712
  if (!authHeader) {
@@ -2615,6 +2718,7 @@ async function handleSimulatedRequest(request, url) {
2615
2718
  if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
2616
2719
  return failure("Invalid authentication", 401);
2617
2720
  }
2721
+ signedDeploymentId = authPayload.deploymentId;
2618
2722
  }
2619
2723
  if (isStorageHost) {
2620
2724
  return handleStorageRequest(request, url);
@@ -2635,6 +2739,9 @@ async function handleSimulatedRequest(request, url) {
2635
2739
  if (edgeMatch) {
2636
2740
  return handleEdgeRequest(request, edgeMatch[1] ?? "");
2637
2741
  }
2742
+ if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
2743
+ return handleStarlensExtract(request, signedDeploymentId);
2744
+ }
2638
2745
  if (dataStoreMatch) {
2639
2746
  const subPath = dataStoreMatch[1] ?? "";
2640
2747
  const db = requireDb();
@@ -2656,7 +2763,7 @@ async function handleSimulatedRequest(request, url) {
2656
2763
  }
2657
2764
  }
2658
2765
  return failure(
2659
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
2766
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
2660
2767
  404
2661
2768
  );
2662
2769
  }
@@ -2824,6 +2931,7 @@ async function createTestApp(options = {}) {
2824
2931
  const storage = createStorage();
2825
2932
  const messages = createMessages();
2826
2933
  const edge = createEdge();
2934
+ const starlens = createStarlens();
2827
2935
  const app = {
2828
2936
  db,
2829
2937
  inbox,
@@ -2832,6 +2940,7 @@ async function createTestApp(options = {}) {
2832
2940
  storage,
2833
2941
  messages,
2834
2942
  edge,
2943
+ starlens,
2835
2944
  async query(sql, params = []) {
2836
2945
  const result = await db.query(sql, params);
2837
2946
  return result.rows;
@@ -2873,6 +2982,7 @@ async function createTestApp(options = {}) {
2873
2982
  storage.clear();
2874
2983
  messages.clear();
2875
2984
  edge.clear();
2985
+ starlens.clear();
2876
2986
  },
2877
2987
  async close() {
2878
2988
  state.db = null;
@@ -2887,6 +2997,7 @@ async function createTestApp(options = {}) {
2887
2997
  storage.clear();
2888
2998
  messages.clear();
2889
2999
  edge.clear();
3000
+ starlens.clear();
2890
3001
  uninstallFetchRouter();
2891
3002
  await db.close();
2892
3003
  }
package/dist/index.mjs CHANGED
@@ -95,6 +95,9 @@ var state = globalSingleton("state", () => ({
95
95
  edgeBindings: /* @__PURE__ */ new Map(),
96
96
  edgeCapabilities: null,
97
97
  edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
98
+ starlensCalls: [],
99
+ starlensReplies: [],
100
+ starlensDefaultReply: null,
98
101
  allowNetwork: false
99
102
  }));
100
103
  function requireDb() {
@@ -2504,6 +2507,104 @@ function createEdge() {
2504
2507
  };
2505
2508
  }
2506
2509
 
2510
+ // src/simulator/starlens.ts
2511
+ function starlensFailure(error, status, code) {
2512
+ return json({ success: false, error, code }, status);
2513
+ }
2514
+ function nullFlagged(output, issues) {
2515
+ const copy = JSON.parse(JSON.stringify(output ?? null));
2516
+ for (const { path } of issues) {
2517
+ const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
2518
+ const last = keys.pop();
2519
+ const parent = keys.reduce(
2520
+ (o, k) => o != null && typeof o === "object" ? o[k] : void 0,
2521
+ copy
2522
+ );
2523
+ if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
2524
+ parent[last] = null;
2525
+ }
2526
+ }
2527
+ return copy;
2528
+ }
2529
+ async function handleStarlensExtract(request, signedDeploymentId) {
2530
+ const body = await readJsonBody(request);
2531
+ if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2532
+ return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2533
+ }
2534
+ const inputs = Array.isArray(body.inputs) ? body.inputs : [];
2535
+ if (inputs.length === 0) {
2536
+ return starlensFailure(
2537
+ "Validation failed \u2014 inputs: at least one input",
2538
+ 400,
2539
+ "VALIDATION_ERROR"
2540
+ );
2541
+ }
2542
+ const schema = body.schema;
2543
+ if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
2544
+ return starlensFailure(
2545
+ "Validation failed \u2014 schema.properties is required",
2546
+ 400,
2547
+ "VALIDATION_ERROR"
2548
+ );
2549
+ }
2550
+ const call = {
2551
+ version: typeof body.version === "string" ? body.version : "latest",
2552
+ inputs: inputs.map((i) => ({
2553
+ name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
2554
+ mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
2555
+ source: typeof i.url === "string" ? "url" : "data"
2556
+ })),
2557
+ schema,
2558
+ instructions: typeof body.instructions === "string" ? body.instructions : void 0
2559
+ };
2560
+ state.starlensCalls.push(call);
2561
+ const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
2562
+ if (!reply) {
2563
+ return starlensFailure(
2564
+ "[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
2565
+ 502,
2566
+ "model_failed"
2567
+ );
2568
+ }
2569
+ if ("reject" in reply) {
2570
+ return starlensFailure(reply.reject, 422, "unsupported_document");
2571
+ }
2572
+ const issues = reply.issues ?? [];
2573
+ return success({
2574
+ version: call.version === "latest" ? "v1" : call.version,
2575
+ output: nullFlagged(reply.output, issues),
2576
+ issues,
2577
+ usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
2578
+ });
2579
+ }
2580
+ function createStarlens() {
2581
+ return {
2582
+ mock(reply) {
2583
+ state.starlensReplies.push(reply);
2584
+ },
2585
+ reject(reason) {
2586
+ state.starlensReplies.push({ reject: reason });
2587
+ },
2588
+ mockDefault(reply) {
2589
+ state.starlensDefaultReply = reply;
2590
+ },
2591
+ get calls() {
2592
+ return state.starlensCalls;
2593
+ },
2594
+ latestCall() {
2595
+ return state.starlensCalls[state.starlensCalls.length - 1];
2596
+ },
2597
+ clear() {
2598
+ state.starlensCalls = [];
2599
+ state.starlensReplies = [];
2600
+ state.starlensDefaultReply = null;
2601
+ },
2602
+ get count() {
2603
+ return state.starlensCalls.length;
2604
+ }
2605
+ };
2606
+ }
2607
+
2507
2608
  // src/simulator/router.ts
2508
2609
  var fetchHolder = globalSingleton("fetch-holder", () => ({
2509
2610
  originalFetch: null
@@ -2525,7 +2626,8 @@ function requiresDeploymentHmac(request, url) {
2525
2626
  /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
2526
2627
  );
2527
2628
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2528
- return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2629
+ const isStarlens = url.pathname === "/api/starlens/extract";
2630
+ return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2529
2631
  }
2530
2632
  function deploymentIdFromPath(url) {
2531
2633
  const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
@@ -2555,6 +2657,7 @@ async function handleSimulatedRequest(request, url) {
2555
2657
  );
2556
2658
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2557
2659
  const isStorageHost = url.hostname === STORAGE_TEST_HOST;
2660
+ let signedDeploymentId = null;
2558
2661
  if (requiresDeploymentHmac(request, url)) {
2559
2662
  const authHeader = request.headers.get("X-Stardeck-Auth");
2560
2663
  if (!authHeader) {
@@ -2566,6 +2669,7 @@ async function handleSimulatedRequest(request, url) {
2566
2669
  if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
2567
2670
  return failure("Invalid authentication", 401);
2568
2671
  }
2672
+ signedDeploymentId = authPayload.deploymentId;
2569
2673
  }
2570
2674
  if (isStorageHost) {
2571
2675
  return handleStorageRequest(request, url);
@@ -2586,6 +2690,9 @@ async function handleSimulatedRequest(request, url) {
2586
2690
  if (edgeMatch) {
2587
2691
  return handleEdgeRequest(request, edgeMatch[1] ?? "");
2588
2692
  }
2693
+ if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
2694
+ return handleStarlensExtract(request, signedDeploymentId);
2695
+ }
2589
2696
  if (dataStoreMatch) {
2590
2697
  const subPath = dataStoreMatch[1] ?? "";
2591
2698
  const db = requireDb();
@@ -2607,7 +2714,7 @@ async function handleSimulatedRequest(request, url) {
2607
2714
  }
2608
2715
  }
2609
2716
  return failure(
2610
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
2717
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
2611
2718
  404
2612
2719
  );
2613
2720
  }
@@ -2775,6 +2882,7 @@ async function createTestApp(options = {}) {
2775
2882
  const storage = createStorage();
2776
2883
  const messages = createMessages();
2777
2884
  const edge = createEdge();
2885
+ const starlens = createStarlens();
2778
2886
  const app = {
2779
2887
  db,
2780
2888
  inbox,
@@ -2783,6 +2891,7 @@ async function createTestApp(options = {}) {
2783
2891
  storage,
2784
2892
  messages,
2785
2893
  edge,
2894
+ starlens,
2786
2895
  async query(sql, params = []) {
2787
2896
  const result = await db.query(sql, params);
2788
2897
  return result.rows;
@@ -2824,6 +2933,7 @@ async function createTestApp(options = {}) {
2824
2933
  storage.clear();
2825
2934
  messages.clear();
2826
2935
  edge.clear();
2936
+ starlens.clear();
2827
2937
  },
2828
2938
  async close() {
2829
2939
  state.db = null;
@@ -2838,6 +2948,7 @@ async function createTestApp(options = {}) {
2838
2948
  storage.clear();
2839
2949
  messages.clear();
2840
2950
  edge.clear();
2951
+ starlens.clear();
2841
2952
  uninstallFetchRouter();
2842
2953
  await db.close();
2843
2954
  }
@@ -74,6 +74,9 @@ var state = globalSingleton("state", () => ({
74
74
  edgeBindings: /* @__PURE__ */ new Map(),
75
75
  edgeCapabilities: null,
76
76
  edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
77
+ starlensCalls: [],
78
+ starlensReplies: [],
79
+ starlensDefaultReply: null,
77
80
  allowNetwork: false
78
81
  }));
79
82
 
@@ -47,6 +47,9 @@ var state = globalSingleton("state", () => ({
47
47
  edgeBindings: /* @__PURE__ */ new Map(),
48
48
  edgeCapabilities: null,
49
49
  edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
50
+ starlensCalls: [],
51
+ starlensReplies: [],
52
+ starlensDefaultReply: null,
50
53
  allowNetwork: false
51
54
  }));
52
55
 
package/dist/setup.js CHANGED
@@ -68,6 +68,9 @@ var state = globalSingleton("state", () => ({
68
68
  edgeBindings: /* @__PURE__ */ new Map(),
69
69
  edgeCapabilities: null,
70
70
  edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
71
+ starlensCalls: [],
72
+ starlensReplies: [],
73
+ starlensDefaultReply: null,
71
74
  allowNetwork: false
72
75
  }));
73
76
  function requireDb() {
@@ -2000,6 +2003,77 @@ async function handleEdgeRequest(request, subPath) {
2000
2003
  return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
2001
2004
  }
2002
2005
 
2006
+ // src/simulator/starlens.ts
2007
+ function starlensFailure(error, status, code) {
2008
+ return json({ success: false, error, code }, status);
2009
+ }
2010
+ function nullFlagged(output, issues) {
2011
+ const copy = JSON.parse(JSON.stringify(output ?? null));
2012
+ for (const { path } of issues) {
2013
+ const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
2014
+ const last = keys.pop();
2015
+ const parent = keys.reduce(
2016
+ (o, k) => o != null && typeof o === "object" ? o[k] : void 0,
2017
+ copy
2018
+ );
2019
+ if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
2020
+ parent[last] = null;
2021
+ }
2022
+ }
2023
+ return copy;
2024
+ }
2025
+ async function handleStarlensExtract(request, signedDeploymentId) {
2026
+ const body = await readJsonBody(request);
2027
+ if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2028
+ return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2029
+ }
2030
+ const inputs = Array.isArray(body.inputs) ? body.inputs : [];
2031
+ if (inputs.length === 0) {
2032
+ return starlensFailure(
2033
+ "Validation failed \u2014 inputs: at least one input",
2034
+ 400,
2035
+ "VALIDATION_ERROR"
2036
+ );
2037
+ }
2038
+ const schema = body.schema;
2039
+ if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
2040
+ return starlensFailure(
2041
+ "Validation failed \u2014 schema.properties is required",
2042
+ 400,
2043
+ "VALIDATION_ERROR"
2044
+ );
2045
+ }
2046
+ const call = {
2047
+ version: typeof body.version === "string" ? body.version : "latest",
2048
+ inputs: inputs.map((i) => ({
2049
+ name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
2050
+ mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
2051
+ source: typeof i.url === "string" ? "url" : "data"
2052
+ })),
2053
+ schema,
2054
+ instructions: typeof body.instructions === "string" ? body.instructions : void 0
2055
+ };
2056
+ state.starlensCalls.push(call);
2057
+ const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
2058
+ if (!reply) {
2059
+ return starlensFailure(
2060
+ "[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
2061
+ 502,
2062
+ "model_failed"
2063
+ );
2064
+ }
2065
+ if ("reject" in reply) {
2066
+ return starlensFailure(reply.reject, 422, "unsupported_document");
2067
+ }
2068
+ const issues = reply.issues ?? [];
2069
+ return success({
2070
+ version: call.version === "latest" ? "v1" : call.version,
2071
+ output: nullFlagged(reply.output, issues),
2072
+ issues,
2073
+ usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
2074
+ });
2075
+ }
2076
+
2003
2077
  // src/simulator/router.ts
2004
2078
  var fetchHolder = globalSingleton("fetch-holder", () => ({
2005
2079
  originalFetch: null
@@ -2021,7 +2095,8 @@ function requiresDeploymentHmac(request, url) {
2021
2095
  /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
2022
2096
  );
2023
2097
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2024
- return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2098
+ const isStarlens = url.pathname === "/api/starlens/extract";
2099
+ return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2025
2100
  }
2026
2101
  function deploymentIdFromPath(url) {
2027
2102
  const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
@@ -2051,6 +2126,7 @@ async function handleSimulatedRequest(request, url) {
2051
2126
  );
2052
2127
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2053
2128
  const isStorageHost = url.hostname === STORAGE_TEST_HOST;
2129
+ let signedDeploymentId = null;
2054
2130
  if (requiresDeploymentHmac(request, url)) {
2055
2131
  const authHeader = request.headers.get("X-Stardeck-Auth");
2056
2132
  if (!authHeader) {
@@ -2062,6 +2138,7 @@ async function handleSimulatedRequest(request, url) {
2062
2138
  if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
2063
2139
  return failure("Invalid authentication", 401);
2064
2140
  }
2141
+ signedDeploymentId = authPayload.deploymentId;
2065
2142
  }
2066
2143
  if (isStorageHost) {
2067
2144
  return handleStorageRequest(request, url);
@@ -2082,6 +2159,9 @@ async function handleSimulatedRequest(request, url) {
2082
2159
  if (edgeMatch) {
2083
2160
  return handleEdgeRequest(request, edgeMatch[1] ?? "");
2084
2161
  }
2162
+ if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
2163
+ return handleStarlensExtract(request, signedDeploymentId);
2164
+ }
2085
2165
  if (dataStoreMatch) {
2086
2166
  const subPath = dataStoreMatch[1] ?? "";
2087
2167
  const db = requireDb();
@@ -2103,7 +2183,7 @@ async function handleSimulatedRequest(request, url) {
2103
2183
  }
2104
2184
  }
2105
2185
  return failure(
2106
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
2186
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
2107
2187
  404
2108
2188
  );
2109
2189
  }
package/dist/setup.mjs CHANGED
@@ -44,6 +44,9 @@ var state = globalSingleton("state", () => ({
44
44
  edgeBindings: /* @__PURE__ */ new Map(),
45
45
  edgeCapabilities: null,
46
46
  edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
47
+ starlensCalls: [],
48
+ starlensReplies: [],
49
+ starlensDefaultReply: null,
47
50
  allowNetwork: false
48
51
  }));
49
52
  function requireDb() {
@@ -1976,6 +1979,77 @@ async function handleEdgeRequest(request, subPath) {
1976
1979
  return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
1977
1980
  }
1978
1981
 
1982
+ // src/simulator/starlens.ts
1983
+ function starlensFailure(error, status, code) {
1984
+ return json({ success: false, error, code }, status);
1985
+ }
1986
+ function nullFlagged(output, issues) {
1987
+ const copy = JSON.parse(JSON.stringify(output ?? null));
1988
+ for (const { path } of issues) {
1989
+ const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
1990
+ const last = keys.pop();
1991
+ const parent = keys.reduce(
1992
+ (o, k) => o != null && typeof o === "object" ? o[k] : void 0,
1993
+ copy
1994
+ );
1995
+ if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
1996
+ parent[last] = null;
1997
+ }
1998
+ }
1999
+ return copy;
2000
+ }
2001
+ async function handleStarlensExtract(request, signedDeploymentId) {
2002
+ const body = await readJsonBody(request);
2003
+ if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2004
+ return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2005
+ }
2006
+ const inputs = Array.isArray(body.inputs) ? body.inputs : [];
2007
+ if (inputs.length === 0) {
2008
+ return starlensFailure(
2009
+ "Validation failed \u2014 inputs: at least one input",
2010
+ 400,
2011
+ "VALIDATION_ERROR"
2012
+ );
2013
+ }
2014
+ const schema = body.schema;
2015
+ if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
2016
+ return starlensFailure(
2017
+ "Validation failed \u2014 schema.properties is required",
2018
+ 400,
2019
+ "VALIDATION_ERROR"
2020
+ );
2021
+ }
2022
+ const call = {
2023
+ version: typeof body.version === "string" ? body.version : "latest",
2024
+ inputs: inputs.map((i) => ({
2025
+ name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
2026
+ mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
2027
+ source: typeof i.url === "string" ? "url" : "data"
2028
+ })),
2029
+ schema,
2030
+ instructions: typeof body.instructions === "string" ? body.instructions : void 0
2031
+ };
2032
+ state.starlensCalls.push(call);
2033
+ const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
2034
+ if (!reply) {
2035
+ return starlensFailure(
2036
+ "[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
2037
+ 502,
2038
+ "model_failed"
2039
+ );
2040
+ }
2041
+ if ("reject" in reply) {
2042
+ return starlensFailure(reply.reject, 422, "unsupported_document");
2043
+ }
2044
+ const issues = reply.issues ?? [];
2045
+ return success({
2046
+ version: call.version === "latest" ? "v1" : call.version,
2047
+ output: nullFlagged(reply.output, issues),
2048
+ issues,
2049
+ usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
2050
+ });
2051
+ }
2052
+
1979
2053
  // src/simulator/router.ts
1980
2054
  var fetchHolder = globalSingleton("fetch-holder", () => ({
1981
2055
  originalFetch: null
@@ -1997,7 +2071,8 @@ function requiresDeploymentHmac(request, url) {
1997
2071
  /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
1998
2072
  );
1999
2073
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2000
- return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2074
+ const isStarlens = url.pathname === "/api/starlens/extract";
2075
+ return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2001
2076
  }
2002
2077
  function deploymentIdFromPath(url) {
2003
2078
  const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
@@ -2027,6 +2102,7 @@ async function handleSimulatedRequest(request, url) {
2027
2102
  );
2028
2103
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2029
2104
  const isStorageHost = url.hostname === STORAGE_TEST_HOST;
2105
+ let signedDeploymentId = null;
2030
2106
  if (requiresDeploymentHmac(request, url)) {
2031
2107
  const authHeader = request.headers.get("X-Stardeck-Auth");
2032
2108
  if (!authHeader) {
@@ -2038,6 +2114,7 @@ async function handleSimulatedRequest(request, url) {
2038
2114
  if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
2039
2115
  return failure("Invalid authentication", 401);
2040
2116
  }
2117
+ signedDeploymentId = authPayload.deploymentId;
2041
2118
  }
2042
2119
  if (isStorageHost) {
2043
2120
  return handleStorageRequest(request, url);
@@ -2058,6 +2135,9 @@ async function handleSimulatedRequest(request, url) {
2058
2135
  if (edgeMatch) {
2059
2136
  return handleEdgeRequest(request, edgeMatch[1] ?? "");
2060
2137
  }
2138
+ if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
2139
+ return handleStarlensExtract(request, signedDeploymentId);
2140
+ }
2061
2141
  if (dataStoreMatch) {
2062
2142
  const subPath = dataStoreMatch[1] ?? "";
2063
2143
  const db = requireDb();
@@ -2079,7 +2159,7 @@ async function handleSimulatedRequest(request, url) {
2079
2159
  }
2080
2160
  }
2081
2161
  return failure(
2082
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
2162
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
2083
2163
  404
2084
2164
  );
2085
2165
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/testing",
3
- "version": "0.14.0",
3
+ "version": "0.15.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",
@@ -70,7 +70,8 @@
70
70
  "@stardeck-customer-apps/payments-sdk": ">=0.1.0",
71
71
  "@stardeck-customer-apps/storage-sdk": ">=0.1.0",
72
72
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
73
- "vitest": ">=2.0.0"
73
+ "vitest": ">=2.0.0",
74
+ "@stardeck-customer-apps/starlens": ">=0.1.0"
74
75
  },
75
76
  "peerDependenciesMeta": {
76
77
  "@stardeck-customer-apps/integrations-sdk": {
@@ -87,6 +88,9 @@
87
88
  },
88
89
  "next": {
89
90
  "optional": true
91
+ },
92
+ "@stardeck-customer-apps/starlens": {
93
+ "optional": true
90
94
  }
91
95
  },
92
96
  "devDependencies": {
@@ -106,6 +110,8 @@
106
110
  "tsup": "^8.0.0",
107
111
  "typescript": "^5.0.0",
108
112
  "typescript-eslint": "^8.0.0",
109
- "vitest": "^3.2.4"
113
+ "vitest": "^3.2.4",
114
+ "@stardeck-customer-apps/starlens": "*",
115
+ "zod": "^4.3.5"
110
116
  }
111
117
  }