@stardeck-customer-apps/testing 0.12.0 → 0.14.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
@@ -193,6 +193,21 @@ await app.payments.deliverBeamEvent(beamPost, {
193
193
  // charge-failure or expiry event for bolt payments.
194
194
  // EXPIRED is derived from expiresAt on read, never stored.
195
195
 
196
+ // Fail exactly one real SDK call, then let the next poll recover normally.
197
+ app.payments.failNext("beam.getPaymentLinkStatus", {
198
+ status: 503,
199
+ message: "Beam is temporarily unavailable",
200
+ });
201
+ await expect(payments.getPaymentLinkStatus("plink_test_1")).rejects.toMatchObject({
202
+ statusCode: 503,
203
+ });
204
+
205
+ // Network failures reject fetch instead of returning a PaymentError response.
206
+ app.payments.failNext("stripe.getCheckoutSessionStatus", {
207
+ kind: "network",
208
+ message: "connection reset",
209
+ });
210
+
196
211
  // File upload
197
212
  const storage = new StorageClient();
198
213
  await storage.upload(new File(["hi"], "note.txt", { type: "text/plain" }));
@@ -280,6 +295,32 @@ expect(app.edge.latestDisplay()?.action).toBe("show");
280
295
  - `allowNetwork` — permit non-Stardeck outbound fetches (off by default).
281
296
  - `app.reset()` — wipe + re-apply schema + re-seed + clear inbox/sessions.
282
297
  Call in `beforeEach`.
298
+ - `app.reset({ mode: "data" })` — available in testing SDK **0.14.0+**. Faster
299
+ opt-in for stable schemas: truncate ordinary tables in `public`, restart their
300
+ owned serial/identity sequences, re-run the data-only seed, and clear the same
301
+ simulator/user/session state as the default. It preserves tables and other
302
+ schema objects instead of rebuilding them. The default remains full schema
303
+ reset; `app.reset({ mode: "schema" })` explicitly selects that default.
304
+
305
+ ```ts
306
+ // Only after verifying this suite keeps its schema fixed and uses a data-only seed:
307
+ beforeEach(() => app.reset({ mode: "data" }));
308
+ ```
309
+
310
+ Keep `app.reset()` for tests that change schema or depend on INSERTs in the
311
+ schema script. Also keep full reset for independent sequences, materialized
312
+ views, partitioned/foreign tables, temporary tables, and custom triggers or
313
+ cross-schema dependencies. Data mode does not restore those states or detect
314
+ eligibility; inspect both the tests and seed before opting in. All required
315
+ baseline rows must come from the data-only seed callback, not schema SQL.
316
+ Data mode refuses foreign keys requiring a cross-schema cascade. Database and
317
+ seed errors reject the reset; fix them before running the next test.
318
+
319
+ Do not use `it.concurrent` or `test.concurrent` with a shared test app: user,
320
+ database, and simulator state are shared within the file. Keep file isolation
321
+ enabled and let Vitest parallelize files. This option does not change workers
322
+ or the platform's test-run deadline.
323
+
283
324
  - `app.asUser(partial?)` — sign in a test user; `getSession()` in app code
284
325
  resolves to it. `app.signOut()` clears.
285
326
  - `app.issueSession(partial?)` — mint access/refresh tokens for cookie-based
@@ -302,6 +343,8 @@ externalId })` helper models a trusted platform/channel link, and
302
343
  identities through the integrations SDK.
303
344
  - `app.payments` — checkouts created through payments-sdk:
304
345
  `.checkouts`, `.latest()`, `.setProducts()`, `.markPaid(id)`,
346
+ `.setSessionStatus(id, status)`, `.setPaymentLinkStatus(id, status)`,
347
+ `.failNext(operation, failure)`,
305
348
  `.deliverStripeEvent(handler, event)`, `.deliverBeamEvent(handler, event)`,
306
349
  Bolt+ controls: `.approveBoltIntent(id)`, `.declineBoltIntent(id)`,
307
350
  `.expireBoltIntent(id)`, `.listBoltIntents()`, `.getBoltIntent(id)`,
@@ -385,7 +428,21 @@ cursor, limit })`; `list()` intentionally keeps its previous array shape.
385
428
  connections/intents/charges, product list) — captured in `app.payments`;
386
429
  fulfill checkout via `markPaid` (poll) or `deliverStripeEvent` /
387
430
  `deliverBeamEvent` (webhook push). Drive a terminal without hardware via
388
- `approveBoltIntent` / `declineBoltIntent` / `expireBoltIntent`.
431
+ `approveBoltIntent` / `declineBoltIntent` / `expireBoltIntent`. Beam payment-link
432
+ polling returns the app's original `order.referenceId`; playground project
433
+ scoping is an internal control-plane detail and must not be handled in app code.
434
+ - Payment outcomes are controllable without bypassing the SDK: Stripe sessions
435
+ support `open`, `complete`, and `expired` with `paid`, `unpaid`, or
436
+ `no_payment_required`; Beam links support `ACTIVE`, `DISABLED`, `EXPIRED`,
437
+ `PAID`, `VOIDED`, and `REFUNDED`; Bolt+ supports approve, decline, expiry, and
438
+ cancellation through the real SDK call. Use `app.payments.failNext()` for
439
+ one-shot HTTP or network failures during Stripe checkout creation/status polling
440
+ and Beam payment-link creation/status polling. The operation names are
441
+ `stripe.createCheckout`, `stripe.getCheckoutSessionStatus`,
442
+ `beam.createPaymentLink`, and `beam.getPaymentLinkStatus`. HTTP failures support
443
+ statuses 400, 402, 409, 429, 500, and 503 plus optional `code`, `details`, and
444
+ `retryAfterSeconds`; `{ kind: "network", message }` rejects fetch instead.
445
+ `app.reset()` clears pending failures.
389
446
  - `StorageClient` (upload/list/get/patch/delete, presigned upload) — captured
390
447
  in `app.storage` against the simulated `STORAGE_URL` host.
391
448
  - `client.slack` / `client.line` / `client.facebook` send endpoints — captured
package/dist/index.d.mts CHANGED
@@ -138,6 +138,18 @@ interface CapturedCheckout {
138
138
  metadata?: Record<string, string>;
139
139
  createdAt: Date;
140
140
  }
141
+ type PaymentSimulatorOperation = "stripe.createCheckout" | "stripe.getCheckoutSessionStatus" | "beam.createPaymentLink" | "beam.getPaymentLinkStatus";
142
+ type PaymentSimulatorFailure = {
143
+ kind?: "http";
144
+ status: 400 | 402 | 409 | 429 | 500 | 503;
145
+ message: string;
146
+ code?: string;
147
+ details?: unknown;
148
+ retryAfterSeconds?: number;
149
+ } | {
150
+ kind: "network";
151
+ message: string;
152
+ };
141
153
  /** Simulated Bolt+ connection as returned by the control-plane store API. */
142
154
  interface SimBoltConnection {
143
155
  id: string;
@@ -189,6 +201,8 @@ interface TestPayments {
189
201
  get checkouts(): CapturedCheckout[];
190
202
  latest(): CapturedCheckout | undefined;
191
203
  setProducts(products: _stardeck_customer_apps_payments_sdk.Product[]): void;
204
+ /** Fail the next matching SDK request once, then resume normal simulator behavior. */
205
+ failNext(operation: PaymentSimulatorOperation, failure: PaymentSimulatorFailure): void;
192
206
  markPaid(id: string): void;
193
207
  setSessionStatus(id: string, status: Partial<_stardeck_customer_apps_payments_sdk.CheckoutSessionStatus>): void;
194
208
  setPaymentLinkStatus(id: string, status: _stardeck_customer_apps_payments_sdk.BeamPaymentLinkStatus): void;
@@ -506,8 +520,15 @@ interface TestApp {
506
520
  * control-plane verify endpoint resolves these tokens back to the user.
507
521
  */
508
522
  issueSession(user?: Partial<TestUser>): SessionTokens;
509
- /** Drop all tables, re-apply the schema, re-run the seed, clear inbox/user. */
510
- reset(): Promise<void>;
523
+ /**
524
+ * Rebuild the schema (default), re-run the seed, and clear simulator/user state.
525
+ * `mode: "data"` instead truncates ordinary public tables and restarts owned
526
+ * sequences. Use only with a stable schema and data-only seed; it does not
527
+ * restore DDL changes, schema-script data, or independent sequences.
528
+ */
529
+ reset(options?: {
530
+ mode: "schema" | "data";
531
+ }): Promise<void>;
511
532
  /** Tear down the app and restore global fetch. */
512
533
  close(): Promise<void>;
513
534
  }
@@ -550,4 +571,4 @@ declare const TEST_ENV_DEFAULTS: {
550
571
  /** Default location of the DDL snapshot written by `generate-types`. */
551
572
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
552
573
 
553
- 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 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 };
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 };
package/dist/index.d.ts CHANGED
@@ -138,6 +138,18 @@ interface CapturedCheckout {
138
138
  metadata?: Record<string, string>;
139
139
  createdAt: Date;
140
140
  }
141
+ type PaymentSimulatorOperation = "stripe.createCheckout" | "stripe.getCheckoutSessionStatus" | "beam.createPaymentLink" | "beam.getPaymentLinkStatus";
142
+ type PaymentSimulatorFailure = {
143
+ kind?: "http";
144
+ status: 400 | 402 | 409 | 429 | 500 | 503;
145
+ message: string;
146
+ code?: string;
147
+ details?: unknown;
148
+ retryAfterSeconds?: number;
149
+ } | {
150
+ kind: "network";
151
+ message: string;
152
+ };
141
153
  /** Simulated Bolt+ connection as returned by the control-plane store API. */
142
154
  interface SimBoltConnection {
143
155
  id: string;
@@ -189,6 +201,8 @@ interface TestPayments {
189
201
  get checkouts(): CapturedCheckout[];
190
202
  latest(): CapturedCheckout | undefined;
191
203
  setProducts(products: _stardeck_customer_apps_payments_sdk.Product[]): void;
204
+ /** Fail the next matching SDK request once, then resume normal simulator behavior. */
205
+ failNext(operation: PaymentSimulatorOperation, failure: PaymentSimulatorFailure): void;
192
206
  markPaid(id: string): void;
193
207
  setSessionStatus(id: string, status: Partial<_stardeck_customer_apps_payments_sdk.CheckoutSessionStatus>): void;
194
208
  setPaymentLinkStatus(id: string, status: _stardeck_customer_apps_payments_sdk.BeamPaymentLinkStatus): void;
@@ -506,8 +520,15 @@ interface TestApp {
506
520
  * control-plane verify endpoint resolves these tokens back to the user.
507
521
  */
508
522
  issueSession(user?: Partial<TestUser>): SessionTokens;
509
- /** Drop all tables, re-apply the schema, re-run the seed, clear inbox/user. */
510
- reset(): Promise<void>;
523
+ /**
524
+ * Rebuild the schema (default), re-run the seed, and clear simulator/user state.
525
+ * `mode: "data"` instead truncates ordinary public tables and restarts owned
526
+ * sequences. Use only with a stable schema and data-only seed; it does not
527
+ * restore DDL changes, schema-script data, or independent sequences.
528
+ */
529
+ reset(options?: {
530
+ mode: "schema" | "data";
531
+ }): Promise<void>;
511
532
  /** Tear down the app and restore global fetch. */
512
533
  close(): Promise<void>;
513
534
  }
@@ -550,4 +571,4 @@ declare const TEST_ENV_DEFAULTS: {
550
571
  /** Default location of the DDL snapshot written by `generate-types`. */
551
572
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
552
573
 
553
- 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 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 };
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 };
package/dist/index.js CHANGED
@@ -73,6 +73,19 @@ async function applySchema(db, schemaSql) {
73
73
  async function dropAllTables(db) {
74
74
  await db.exec(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`);
75
75
  }
76
+ async function listPublicTables(db) {
77
+ const { rows } = await db.query(`
78
+ SELECT format('%I.%I', n.nspname, c.relname) AS name
79
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
80
+ WHERE n.nspname = 'public' AND c.relkind = 'r'
81
+ ORDER BY c.relname
82
+ `);
83
+ return rows.map(({ name }) => name);
84
+ }
85
+ async function truncateAllTables(db) {
86
+ const tables = await listPublicTables(db);
87
+ if (tables.length) await db.exec(`TRUNCATE TABLE ${tables.join(", ")} RESTART IDENTITY`);
88
+ }
76
89
  var identityParsers = null;
77
90
  function getIdentityParsers() {
78
91
  if (!identityParsers) {
@@ -108,6 +121,7 @@ var state = globalSingleton("state", () => ({
108
121
  checkoutCounter: 0,
109
122
  sessionStatuses: /* @__PURE__ */ new Map(),
110
123
  paymentLinks: /* @__PURE__ */ new Map(),
124
+ paymentFailures: [],
111
125
  products: [],
112
126
  boltConnections: /* @__PURE__ */ new Map(),
113
127
  boltUsedPairingCodes: /* @__PURE__ */ new Set(),
@@ -1274,6 +1288,43 @@ function payErr(error, status = 400, code, details) {
1274
1288
  if (details !== void 0) body.details = details;
1275
1289
  return json(body, status);
1276
1290
  }
1291
+ function paymentOperationForRequest(request, pathname) {
1292
+ if (request.method === "POST" && /^\/api\/store\/beam\/[^/]+\/payment-links$/.test(pathname)) {
1293
+ return "beam.createPaymentLink";
1294
+ }
1295
+ if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/payment-links\/[^/]+$/.test(pathname)) {
1296
+ return "beam.getPaymentLinkStatus";
1297
+ }
1298
+ if (request.method === "POST" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout$/.test(pathname)) {
1299
+ return "stripe.createCheckout";
1300
+ }
1301
+ if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions\/[^/]+$/.test(pathname)) {
1302
+ return "stripe.getCheckoutSessionStatus";
1303
+ }
1304
+ return null;
1305
+ }
1306
+ function consumePaymentFailure(request, pathname) {
1307
+ const operation = paymentOperationForRequest(request, pathname);
1308
+ if (!operation) return null;
1309
+ const index = state.paymentFailures.findIndex((entry) => entry.operation === operation);
1310
+ if (index === -1) return null;
1311
+ const [{ failure: failure2 }] = state.paymentFailures.splice(index, 1);
1312
+ if (failure2.kind === "network") {
1313
+ throw new TypeError(failure2.message);
1314
+ }
1315
+ const headers = new Headers({ "Content-Type": "application/json" });
1316
+ if (failure2.retryAfterSeconds !== void 0) {
1317
+ headers.set("Retry-After", String(failure2.retryAfterSeconds));
1318
+ }
1319
+ return new Response(
1320
+ JSON.stringify({
1321
+ error: failure2.message,
1322
+ code: failure2.code ?? "REQUEST_ERROR",
1323
+ ...failure2.details !== void 0 ? { details: failure2.details } : {}
1324
+ }),
1325
+ { status: failure2.status, headers }
1326
+ );
1327
+ }
1277
1328
  function isBoltPaymentMethod(value) {
1278
1329
  return typeof value === "string" && BEAM_BOLT_PAYMENT_METHODS.includes(value);
1279
1330
  }
@@ -1417,6 +1468,8 @@ async function deliverEvent(envelope, handler, options) {
1417
1468
  }
1418
1469
  async function handlePaymentsRequest(request, url) {
1419
1470
  const pathname = url.pathname;
1471
+ const injectedFailure = consumePaymentFailure(request, pathname);
1472
+ if (injectedFailure) return injectedFailure;
1420
1473
  if (/\/billing-portal$/.test(pathname)) {
1421
1474
  return payErr("Not found", 404, "NOT_FOUND");
1422
1475
  }
@@ -1711,6 +1764,9 @@ function createPayments() {
1711
1764
  setProducts(products) {
1712
1765
  state.products = products;
1713
1766
  },
1767
+ failNext(operation, failure2) {
1768
+ state.paymentFailures.push({ operation, failure: failure2 });
1769
+ },
1714
1770
  markPaid(id) {
1715
1771
  const session = state.sessionStatuses.get(id);
1716
1772
  if (session) {
@@ -1820,6 +1876,7 @@ function createPayments() {
1820
1876
  state.checkoutCounter = 0;
1821
1877
  state.sessionStatuses.clear();
1822
1878
  state.paymentLinks.clear();
1879
+ state.paymentFailures = [];
1823
1880
  state.products = [];
1824
1881
  state.boltConnections.clear();
1825
1882
  state.boltUsedPairingCodes.clear();
@@ -2797,9 +2854,14 @@ async function createTestApp(options = {}) {
2797
2854
  state.refreshSessions.set(tokens.refreshToken, fullUser);
2798
2855
  return tokens;
2799
2856
  },
2800
- async reset() {
2801
- await dropAllTables(db);
2802
- await initialize();
2857
+ async reset(resetOptions) {
2858
+ if (resetOptions?.mode === "data") {
2859
+ await truncateAllTables(db);
2860
+ if (options.seed) await options.seed(db);
2861
+ } else {
2862
+ await dropAllTables(db);
2863
+ await initialize();
2864
+ }
2803
2865
  state.currentUser = null;
2804
2866
  state.sessions.clear();
2805
2867
  state.refreshSessions.clear();
package/dist/index.mjs CHANGED
@@ -24,6 +24,19 @@ async function applySchema(db, schemaSql) {
24
24
  async function dropAllTables(db) {
25
25
  await db.exec(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`);
26
26
  }
27
+ async function listPublicTables(db) {
28
+ const { rows } = await db.query(`
29
+ SELECT format('%I.%I', n.nspname, c.relname) AS name
30
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
31
+ WHERE n.nspname = 'public' AND c.relkind = 'r'
32
+ ORDER BY c.relname
33
+ `);
34
+ return rows.map(({ name }) => name);
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
+ }
27
40
  var identityParsers = null;
28
41
  function getIdentityParsers() {
29
42
  if (!identityParsers) {
@@ -59,6 +72,7 @@ var state = globalSingleton("state", () => ({
59
72
  checkoutCounter: 0,
60
73
  sessionStatuses: /* @__PURE__ */ new Map(),
61
74
  paymentLinks: /* @__PURE__ */ new Map(),
75
+ paymentFailures: [],
62
76
  products: [],
63
77
  boltConnections: /* @__PURE__ */ new Map(),
64
78
  boltUsedPairingCodes: /* @__PURE__ */ new Set(),
@@ -1225,6 +1239,43 @@ function payErr(error, status = 400, code, details) {
1225
1239
  if (details !== void 0) body.details = details;
1226
1240
  return json(body, status);
1227
1241
  }
1242
+ function paymentOperationForRequest(request, pathname) {
1243
+ if (request.method === "POST" && /^\/api\/store\/beam\/[^/]+\/payment-links$/.test(pathname)) {
1244
+ return "beam.createPaymentLink";
1245
+ }
1246
+ if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/payment-links\/[^/]+$/.test(pathname)) {
1247
+ return "beam.getPaymentLinkStatus";
1248
+ }
1249
+ if (request.method === "POST" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout$/.test(pathname)) {
1250
+ return "stripe.createCheckout";
1251
+ }
1252
+ if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions\/[^/]+$/.test(pathname)) {
1253
+ return "stripe.getCheckoutSessionStatus";
1254
+ }
1255
+ return null;
1256
+ }
1257
+ function consumePaymentFailure(request, pathname) {
1258
+ const operation = paymentOperationForRequest(request, pathname);
1259
+ if (!operation) return null;
1260
+ const index = state.paymentFailures.findIndex((entry) => entry.operation === operation);
1261
+ if (index === -1) return null;
1262
+ const [{ failure: failure2 }] = state.paymentFailures.splice(index, 1);
1263
+ if (failure2.kind === "network") {
1264
+ throw new TypeError(failure2.message);
1265
+ }
1266
+ const headers = new Headers({ "Content-Type": "application/json" });
1267
+ if (failure2.retryAfterSeconds !== void 0) {
1268
+ headers.set("Retry-After", String(failure2.retryAfterSeconds));
1269
+ }
1270
+ return new Response(
1271
+ JSON.stringify({
1272
+ error: failure2.message,
1273
+ code: failure2.code ?? "REQUEST_ERROR",
1274
+ ...failure2.details !== void 0 ? { details: failure2.details } : {}
1275
+ }),
1276
+ { status: failure2.status, headers }
1277
+ );
1278
+ }
1228
1279
  function isBoltPaymentMethod(value) {
1229
1280
  return typeof value === "string" && BEAM_BOLT_PAYMENT_METHODS.includes(value);
1230
1281
  }
@@ -1368,6 +1419,8 @@ async function deliverEvent(envelope, handler, options) {
1368
1419
  }
1369
1420
  async function handlePaymentsRequest(request, url) {
1370
1421
  const pathname = url.pathname;
1422
+ const injectedFailure = consumePaymentFailure(request, pathname);
1423
+ if (injectedFailure) return injectedFailure;
1371
1424
  if (/\/billing-portal$/.test(pathname)) {
1372
1425
  return payErr("Not found", 404, "NOT_FOUND");
1373
1426
  }
@@ -1662,6 +1715,9 @@ function createPayments() {
1662
1715
  setProducts(products) {
1663
1716
  state.products = products;
1664
1717
  },
1718
+ failNext(operation, failure2) {
1719
+ state.paymentFailures.push({ operation, failure: failure2 });
1720
+ },
1665
1721
  markPaid(id) {
1666
1722
  const session = state.sessionStatuses.get(id);
1667
1723
  if (session) {
@@ -1771,6 +1827,7 @@ function createPayments() {
1771
1827
  state.checkoutCounter = 0;
1772
1828
  state.sessionStatuses.clear();
1773
1829
  state.paymentLinks.clear();
1830
+ state.paymentFailures = [];
1774
1831
  state.products = [];
1775
1832
  state.boltConnections.clear();
1776
1833
  state.boltUsedPairingCodes.clear();
@@ -2748,9 +2805,14 @@ async function createTestApp(options = {}) {
2748
2805
  state.refreshSessions.set(tokens.refreshToken, fullUser);
2749
2806
  return tokens;
2750
2807
  },
2751
- async reset() {
2752
- await dropAllTables(db);
2753
- await initialize();
2808
+ async reset(resetOptions) {
2809
+ if (resetOptions?.mode === "data") {
2810
+ await truncateAllTables(db);
2811
+ if (options.seed) await options.seed(db);
2812
+ } else {
2813
+ await dropAllTables(db);
2814
+ await initialize();
2815
+ }
2754
2816
  state.currentUser = null;
2755
2817
  state.sessions.clear();
2756
2818
  state.refreshSessions.clear();
@@ -51,6 +51,7 @@ var state = globalSingleton("state", () => ({
51
51
  checkoutCounter: 0,
52
52
  sessionStatuses: /* @__PURE__ */ new Map(),
53
53
  paymentLinks: /* @__PURE__ */ new Map(),
54
+ paymentFailures: [],
54
55
  products: [],
55
56
  boltConnections: /* @__PURE__ */ new Map(),
56
57
  boltUsedPairingCodes: /* @__PURE__ */ new Set(),
@@ -24,6 +24,7 @@ var state = globalSingleton("state", () => ({
24
24
  checkoutCounter: 0,
25
25
  sessionStatuses: /* @__PURE__ */ new Map(),
26
26
  paymentLinks: /* @__PURE__ */ new Map(),
27
+ paymentFailures: [],
27
28
  products: [],
28
29
  boltConnections: /* @__PURE__ */ new Map(),
29
30
  boltUsedPairingCodes: /* @__PURE__ */ new Set(),
package/dist/setup.js CHANGED
@@ -45,6 +45,7 @@ var state = globalSingleton("state", () => ({
45
45
  checkoutCounter: 0,
46
46
  sessionStatuses: /* @__PURE__ */ new Map(),
47
47
  paymentLinks: /* @__PURE__ */ new Map(),
48
+ paymentFailures: [],
48
49
  products: [],
49
50
  boltConnections: /* @__PURE__ */ new Map(),
50
51
  boltUsedPairingCodes: /* @__PURE__ */ new Set(),
@@ -1012,6 +1013,43 @@ function payErr(error, status = 400, code, details) {
1012
1013
  if (details !== void 0) body.details = details;
1013
1014
  return json(body, status);
1014
1015
  }
1016
+ function paymentOperationForRequest(request, pathname) {
1017
+ if (request.method === "POST" && /^\/api\/store\/beam\/[^/]+\/payment-links$/.test(pathname)) {
1018
+ return "beam.createPaymentLink";
1019
+ }
1020
+ if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/payment-links\/[^/]+$/.test(pathname)) {
1021
+ return "beam.getPaymentLinkStatus";
1022
+ }
1023
+ if (request.method === "POST" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout$/.test(pathname)) {
1024
+ return "stripe.createCheckout";
1025
+ }
1026
+ if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions\/[^/]+$/.test(pathname)) {
1027
+ return "stripe.getCheckoutSessionStatus";
1028
+ }
1029
+ return null;
1030
+ }
1031
+ function consumePaymentFailure(request, pathname) {
1032
+ const operation = paymentOperationForRequest(request, pathname);
1033
+ if (!operation) return null;
1034
+ const index = state.paymentFailures.findIndex((entry) => entry.operation === operation);
1035
+ if (index === -1) return null;
1036
+ const [{ failure: failure2 }] = state.paymentFailures.splice(index, 1);
1037
+ if (failure2.kind === "network") {
1038
+ throw new TypeError(failure2.message);
1039
+ }
1040
+ const headers = new Headers({ "Content-Type": "application/json" });
1041
+ if (failure2.retryAfterSeconds !== void 0) {
1042
+ headers.set("Retry-After", String(failure2.retryAfterSeconds));
1043
+ }
1044
+ return new Response(
1045
+ JSON.stringify({
1046
+ error: failure2.message,
1047
+ code: failure2.code ?? "REQUEST_ERROR",
1048
+ ...failure2.details !== void 0 ? { details: failure2.details } : {}
1049
+ }),
1050
+ { status: failure2.status, headers }
1051
+ );
1052
+ }
1015
1053
  function isBoltPaymentMethod(value) {
1016
1054
  return typeof value === "string" && BEAM_BOLT_PAYMENT_METHODS.includes(value);
1017
1055
  }
@@ -1120,6 +1158,8 @@ function storedStatusesForFilter(statuses) {
1120
1158
  }
1121
1159
  async function handlePaymentsRequest(request, url) {
1122
1160
  const pathname = url.pathname;
1161
+ const injectedFailure = consumePaymentFailure(request, pathname);
1162
+ if (injectedFailure) return injectedFailure;
1123
1163
  if (/\/billing-portal$/.test(pathname)) {
1124
1164
  return payErr("Not found", 404, "NOT_FOUND");
1125
1165
  }
package/dist/setup.mjs CHANGED
@@ -21,6 +21,7 @@ var state = globalSingleton("state", () => ({
21
21
  checkoutCounter: 0,
22
22
  sessionStatuses: /* @__PURE__ */ new Map(),
23
23
  paymentLinks: /* @__PURE__ */ new Map(),
24
+ paymentFailures: [],
24
25
  products: [],
25
26
  boltConnections: /* @__PURE__ */ new Map(),
26
27
  boltUsedPairingCodes: /* @__PURE__ */ new Set(),
@@ -988,6 +989,43 @@ function payErr(error, status = 400, code, details) {
988
989
  if (details !== void 0) body.details = details;
989
990
  return json(body, status);
990
991
  }
992
+ function paymentOperationForRequest(request, pathname) {
993
+ if (request.method === "POST" && /^\/api\/store\/beam\/[^/]+\/payment-links$/.test(pathname)) {
994
+ return "beam.createPaymentLink";
995
+ }
996
+ if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/payment-links\/[^/]+$/.test(pathname)) {
997
+ return "beam.getPaymentLinkStatus";
998
+ }
999
+ if (request.method === "POST" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout$/.test(pathname)) {
1000
+ return "stripe.createCheckout";
1001
+ }
1002
+ if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions\/[^/]+$/.test(pathname)) {
1003
+ return "stripe.getCheckoutSessionStatus";
1004
+ }
1005
+ return null;
1006
+ }
1007
+ function consumePaymentFailure(request, pathname) {
1008
+ const operation = paymentOperationForRequest(request, pathname);
1009
+ if (!operation) return null;
1010
+ const index = state.paymentFailures.findIndex((entry) => entry.operation === operation);
1011
+ if (index === -1) return null;
1012
+ const [{ failure: failure2 }] = state.paymentFailures.splice(index, 1);
1013
+ if (failure2.kind === "network") {
1014
+ throw new TypeError(failure2.message);
1015
+ }
1016
+ const headers = new Headers({ "Content-Type": "application/json" });
1017
+ if (failure2.retryAfterSeconds !== void 0) {
1018
+ headers.set("Retry-After", String(failure2.retryAfterSeconds));
1019
+ }
1020
+ return new Response(
1021
+ JSON.stringify({
1022
+ error: failure2.message,
1023
+ code: failure2.code ?? "REQUEST_ERROR",
1024
+ ...failure2.details !== void 0 ? { details: failure2.details } : {}
1025
+ }),
1026
+ { status: failure2.status, headers }
1027
+ );
1028
+ }
991
1029
  function isBoltPaymentMethod(value) {
992
1030
  return typeof value === "string" && BEAM_BOLT_PAYMENT_METHODS.includes(value);
993
1031
  }
@@ -1096,6 +1134,8 @@ function storedStatusesForFilter(statuses) {
1096
1134
  }
1097
1135
  async function handlePaymentsRequest(request, url) {
1098
1136
  const pathname = url.pathname;
1137
+ const injectedFailure = consumePaymentFailure(request, pathname);
1138
+ if (injectedFailure) return injectedFailure;
1099
1139
  if (/\/billing-portal$/.test(pathname)) {
1100
1140
  return payErr("Not found", 404, "NOT_FOUND");
1101
1141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/testing",
3
- "version": "0.12.0",
3
+ "version": "0.14.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",