@stardeck-customer-apps/testing 0.3.1 → 0.5.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
@@ -67,6 +67,51 @@ describeWorkflow("checkout", () => {
67
67
  });
68
68
  ```
69
69
 
70
+ Payments, storage, and messaging:
71
+
72
+ ```ts
73
+ import { paymentsHandler } from "@stardeck-customer-apps/payments-sdk";
74
+ import { PaymentsServerClient } from "@stardeck-customer-apps/payments-sdk/server";
75
+ import { StorageClient } from "@stardeck-customer-apps/storage-sdk/server";
76
+ import { createIntegrationsClient } from "@stardeck-customer-apps/integrations-sdk";
77
+
78
+ // Stripe checkout + webhook fulfillment
79
+ const payments = new PaymentsServerClient({ /* reads CONTROL_PLANE_URL + DEPLOYMENT_SECRET from env */ });
80
+ const session = await payments.createCheckout({ /* lineItems, successUrl, cancelUrl */ });
81
+ const { POST } = paymentsHandler({
82
+ onStripeWebhook: async (event) => {
83
+ if (event.type === "checkout.session.completed") {
84
+ await app.query(`UPDATE orders SET status = 'paid' WHERE session_id = $1`, [
85
+ (event.data as { object: { id: string } }).object.id,
86
+ ]);
87
+ }
88
+ },
89
+ });
90
+ await app.payments.deliverStripeEvent(POST, {
91
+ type: "checkout.session.completed",
92
+ data: { object: { id: session.id } },
93
+ });
94
+
95
+ // File upload
96
+ const storage = new StorageClient();
97
+ await storage.upload(new File(["hi"], "note.txt", { type: "text/plain" }));
98
+ expect(app.storage.latest()?.filename).toBe("note.txt");
99
+
100
+ // LINE notification
101
+ const integrations = createIntegrationsClient();
102
+ await integrations.line.push("U123", { type: "text", text: "Your order shipped" });
103
+ expect(app.messages.channel("line").to("U123")[0].body.text).toMatch(/shipped/i);
104
+
105
+ // Receipt print + display
106
+ import { createEdgeClient } from "@stardeck-customer-apps/edge-sdk/server";
107
+
108
+ const edge = createEdgeClient();
109
+ await edge.print({ receipt: { total: 42 }, alias: "station-1", openDrawer: true });
110
+ expect(app.edge.latestPrint()?.receipt.total).toBe(42);
111
+ await edge.showDisplay({ alias: "customer-screen", url: "https://app.example.com/kiosk" });
112
+ expect(app.edge.latestDisplay()?.action).toBe("show");
113
+ ```
114
+
70
115
  ## API
71
116
 
72
117
  - `createTestApp(options)` — boots PGlite + the control-plane simulator and
@@ -88,6 +133,16 @@ describeWorkflow("checkout", () => {
88
133
  - `app.identities` — the platform-identity directory created through
89
134
  integrations-sdk `client.identities`: `.get(id)`, `.links(id)`, `.all()`,
90
135
  `.count`, `.clear()`.
136
+ - `app.payments` — checkouts created through payments-sdk:
137
+ `.checkouts`, `.latest()`, `.setProducts()`, `.markPaid(id)`,
138
+ `.deliverStripeEvent(handler, event)`, `.deliverBeamEvent(handler, event)`,
139
+ `.count`, `.clear()`.
140
+ - `app.storage` — uploads through storage-sdk: `.uploads`, `.latest()`, `.count`, `.clear()`.
141
+ - `app.messages` — outbound Slack/LINE/Facebook sends:
142
+ `.all()`, `.latest()`, `.to(recipient)`, `.channel("line")`, `.count`, `.clear()`.
143
+ - `app.edge` — edge print/display/bindings from edge-sdk: `.prints`, `.displays`,
144
+ `.testPrints`, `.latestPrint()`, `.latestDisplay()`, `.bindings`,
145
+ `.seedDevices()`, `.seedPeripherals()`, `.seedBindings()`, `.count`, `.clear()`.
91
146
  - `app.query(sql, params?)` / `app.db` — direct database access for
92
147
  assertions and seeding.
93
148
  - `callRoute(handler, opts)` — invoke an App Router route handler with a real
@@ -111,6 +166,16 @@ describeWorkflow("checkout", () => {
111
166
  persons, attach channel links) — served offline by the simulated directory;
112
167
  `update` replaces the `profile` object (not a merge), like the control plane.
113
168
  Inspect via `app.identities`. Merge/archive are dashboard-only — not simulated.
169
+ - `PaymentsServerClient` (Stripe checkout + Beam payment links, product list) —
170
+ captured in `app.payments`; fulfill via `markPaid` (poll) or
171
+ `deliverStripeEvent` / `deliverBeamEvent` (webhook push).
172
+ - `StorageClient` (upload/list/get/patch/delete, presigned upload) — captured
173
+ in `app.storage` against the simulated `STORAGE_URL` host.
174
+ - `client.slack` / `client.line` / `client.facebook` send endpoints — captured
175
+ in `app.messages`.
176
+ - `createEdgeClient()` (print, pair/unpair, list devices/peripherals, show/clear
177
+ display, test print) — captured in `app.edge`; seed devices/peripherals for
178
+ pairing pickers via `app.edge.seedDevices()` / `seedPeripherals()`.
114
179
  - `next/headers` (`headers()`/`cookies()`) inside handlers under `callRoute`.
115
180
 
116
181
  ## Rules
package/dist/index.d.mts CHANGED
@@ -1,6 +1,27 @@
1
+ import * as _stardeck_customer_apps_payments_sdk from '@stardeck-customer-apps/payments-sdk';
2
+ import * as next_server from 'next/server';
1
3
  import { PGlite } from '@electric-sql/pglite';
2
- import { ModuleSchemaOp, ModuleDataPort, IdentityClient } from '@stardeck-customer-apps/core';
3
4
 
5
+ type RouteHandler<Req extends Request = Request, P = Record<string, string | string[]>> = (request: Req, context: {
6
+ params: Promise<P>;
7
+ }) => Promise<Response> | Response;
8
+ interface CallRouteOptions {
9
+ /** Request path, e.g. "/api/orders". Defaults to "/api/test-route". */
10
+ path?: string;
11
+ method?: string;
12
+ /** JSON body. Implies POST unless `method` is set. */
13
+ body?: unknown;
14
+ searchParams?: Record<string, string>;
15
+ /** Next.js dynamic segment params, e.g. { id: "123" }. */
16
+ params?: Record<string, string | string[]>;
17
+ headers?: Record<string, string>;
18
+ cookies?: Record<string, string>;
19
+ /** Override the active test user for this call only. */
20
+ user?: TestUser | null;
21
+ }
22
+ declare function callRoute<Req extends Request = Request, P = Record<string, string | string[]>>(handler: RouteHandler<Req, P>, options?: CallRouteOptions): Promise<Response>;
23
+
24
+ type PaymentsWebhookHandler = RouteHandler<next_server.NextRequest, Record<string, string | string[] | undefined>>;
4
25
  /**
5
26
  * The user shape returned by project-auth's `getSession()`. Mirrors the
6
27
  * `UserSchema` in @stardeck-customer-apps/project-auth — keep in sync.
@@ -85,6 +106,184 @@ interface TestDirectory {
85
106
  clear(): void;
86
107
  get count(): number;
87
108
  }
109
+ interface CapturedCheckout {
110
+ id: string;
111
+ url: string;
112
+ provider: "stripe" | "beam";
113
+ options: Record<string, unknown>;
114
+ mode?: "payment" | "subscription";
115
+ metadata?: Record<string, string>;
116
+ createdAt: Date;
117
+ }
118
+ interface TestPayments {
119
+ get checkouts(): CapturedCheckout[];
120
+ latest(): CapturedCheckout | undefined;
121
+ setProducts(products: _stardeck_customer_apps_payments_sdk.Product[]): void;
122
+ markPaid(id: string): void;
123
+ setSessionStatus(id: string, status: Partial<_stardeck_customer_apps_payments_sdk.CheckoutSessionStatus>): void;
124
+ setPaymentLinkStatus(id: string, status: _stardeck_customer_apps_payments_sdk.BeamPaymentLinkStatus): void;
125
+ deliverStripeEvent(handler: PaymentsWebhookHandler, event: {
126
+ type: string;
127
+ data: unknown;
128
+ accountId?: string;
129
+ }, options?: {
130
+ path?: string;
131
+ deploymentSecret?: string;
132
+ }): Promise<Response>;
133
+ deliverBeamEvent(handler: PaymentsWebhookHandler, event: {
134
+ type: string;
135
+ payload: Record<string, unknown>;
136
+ }, options?: {
137
+ path?: string;
138
+ deploymentSecret?: string;
139
+ }): Promise<Response>;
140
+ clear(): void;
141
+ get count(): number;
142
+ }
143
+ interface CapturedUpload {
144
+ id: string;
145
+ key: string;
146
+ filename: string;
147
+ contentType: string;
148
+ sizeBytes: number;
149
+ url: string;
150
+ uploadedAt: string;
151
+ isPublic: boolean;
152
+ metadata?: Record<string, string>;
153
+ method: string;
154
+ path: string;
155
+ }
156
+ interface TestStorage {
157
+ get uploads(): CapturedUpload[];
158
+ latest(): CapturedUpload | undefined;
159
+ clear(): void;
160
+ get count(): number;
161
+ }
162
+ interface CapturedMessage {
163
+ channel: "slack" | "line" | "facebook";
164
+ recipient: string;
165
+ body: {
166
+ text?: string;
167
+ blocks?: unknown[];
168
+ threadTs?: string;
169
+ messagingType?: string;
170
+ tag?: string;
171
+ };
172
+ connectionId?: string;
173
+ sentAt: Date;
174
+ }
175
+ interface TestMessages {
176
+ all(): CapturedMessage[];
177
+ latest(): CapturedMessage | undefined;
178
+ to(recipient: string): CapturedMessage[];
179
+ channel(kind: "slack" | "line" | "facebook"): {
180
+ all(): CapturedMessage[];
181
+ latest(): CapturedMessage | undefined;
182
+ to(recipient: string): CapturedMessage[];
183
+ };
184
+ clear(): void;
185
+ get count(): number;
186
+ }
187
+ interface ReceiptPayload {
188
+ header?: {
189
+ lines: string[];
190
+ };
191
+ items?: Array<{
192
+ name: string;
193
+ quantity: number;
194
+ unitPrice: number;
195
+ total: number;
196
+ note?: string;
197
+ }>;
198
+ subtotal?: number;
199
+ tax?: number;
200
+ discount?: number;
201
+ total?: number;
202
+ payments?: Array<{
203
+ method: string;
204
+ amount: number;
205
+ }>;
206
+ footer?: {
207
+ lines: string[];
208
+ };
209
+ barcode?: {
210
+ type: "qr" | "code128";
211
+ data: string;
212
+ };
213
+ rawEscPos?: string;
214
+ }
215
+ interface DeviceInfo {
216
+ id: string;
217
+ displayName: string;
218
+ status: "pairing_pending" | "online" | "offline" | "decommissioned";
219
+ isDefault: boolean;
220
+ lastHeartbeatAt: string | null;
221
+ }
222
+ interface PeripheralInfo {
223
+ id: string;
224
+ displayName: string | null;
225
+ driver: string | null;
226
+ transport: "usb_lp" | "serial" | "tcp" | "usb_raw" | "display";
227
+ connected: boolean;
228
+ lastSeenAt: string | null;
229
+ device: {
230
+ id: string;
231
+ displayName: string;
232
+ status: DeviceInfo["status"];
233
+ };
234
+ }
235
+ type BindingState = "ok" | "peripheral_missing" | "grant_revoked";
236
+ interface BindingInfo {
237
+ alias: string;
238
+ state: BindingState;
239
+ peripheral: {
240
+ id: string;
241
+ displayName: string | null;
242
+ driver: string | null;
243
+ connected: boolean;
244
+ } | null;
245
+ device: {
246
+ id: string;
247
+ displayName: string;
248
+ status: DeviceInfo["status"];
249
+ } | null;
250
+ updatedAt: string;
251
+ }
252
+ interface CapturedPrint {
253
+ jobId: string;
254
+ deploymentId: string;
255
+ alias?: string;
256
+ deviceId?: string;
257
+ peripheralId?: string;
258
+ receipt: ReceiptPayload;
259
+ openDrawer: boolean;
260
+ logo?: boolean;
261
+ copies: number;
262
+ }
263
+ interface CapturedDisplay {
264
+ action: "show" | "clear";
265
+ alias?: string;
266
+ peripheralId?: string;
267
+ url?: string;
268
+ }
269
+ interface CapturedTestPrint {
270
+ alias?: string;
271
+ deviceId?: string;
272
+ peripheralId?: string;
273
+ }
274
+ interface TestEdge {
275
+ get prints(): CapturedPrint[];
276
+ get displays(): CapturedDisplay[];
277
+ get testPrints(): CapturedTestPrint[];
278
+ latestPrint(): CapturedPrint | undefined;
279
+ latestDisplay(): CapturedDisplay | undefined;
280
+ get bindings(): BindingInfo[];
281
+ seedDevices(devices: DeviceInfo[]): void;
282
+ seedPeripherals(peripherals: PeripheralInfo[]): void;
283
+ seedBindings(bindings: BindingInfo[]): void;
284
+ clear(): void;
285
+ get count(): number;
286
+ }
88
287
  interface TestAppOptions {
89
288
  /**
90
289
  * Path to the schema.sql snapshot generated by
@@ -114,6 +313,14 @@ interface TestApp {
114
313
  inbox: TestInbox;
115
314
  /** Inspect the simulated platform-identity directory (`client.identities`). */
116
315
  identities: TestDirectory;
316
+ /** Captured checkouts and webhook delivery helpers for payments-sdk. */
317
+ payments: TestPayments;
318
+ /** Captured file uploads from storage-sdk. */
319
+ storage: TestStorage;
320
+ /** Captured outbound messages from integrations-sdk messaging channels. */
321
+ messages: TestMessages;
322
+ /** Captured edge print/display ops and peripheral bindings from edge-sdk. */
323
+ edge: TestEdge;
117
324
  /** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
118
325
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
119
326
  /**
@@ -141,71 +348,6 @@ interface TestApp {
141
348
  */
142
349
  declare function createTestApp(options?: TestAppOptions): Promise<TestApp>;
143
350
 
144
- /** The minimal slice of a module definition the harness needs to stand it up. */
145
- interface MountableModule {
146
- schema: ModuleSchemaOp[];
147
- }
148
- interface ModuleSeedDeps {
149
- /** Raw-SQL data port over the harness PGlite — what the module engine reads/writes through. */
150
- data: ModuleDataPort;
151
- /** Identity client backed by the simulated platform identity directory. */
152
- identities: IdentityClient;
153
- }
154
- interface CreateModuleAppOptions {
155
- /** Modules whose install schema is rendered to DDL and applied to the test DB. */
156
- modules: MountableModule[];
157
- /**
158
- * Seed run once after the schema is applied, and again on every `reset()`.
159
- * Receives the same data port + identity client the module engine uses, so a
160
- * module's own `seed*` function drops straight in.
161
- */
162
- seed?: (deps: ModuleSeedDeps) => Promise<void>;
163
- /** Allow real outbound network (default false). */
164
- allowNetwork?: boolean;
165
- }
166
- interface ModuleApp extends ModuleSeedDeps {
167
- /** The underlying harness app (db, identities inspector, callRoute, asUser, …). */
168
- app: TestApp;
169
- /** Reset to a clean slate: drop + re-apply the module schema, clear the sim, re-run the seed. */
170
- reset(): Promise<void>;
171
- /** Tear down (restore global fetch, close the db). */
172
- close(): Promise<void>;
173
- }
174
- /**
175
- * Stand a capability module up on the in-process platform simulator — the **L2**
176
- * harness. It renders the module's install schema to real DDL (the SAME renderer
177
- * the control plane uses, drift-tested in apps/web), applies it to PGlite, wires a
178
- * raw-SQL data port over it, and a real `integrations-sdk` identity client talking
179
- * to the simulated identity directory (so scoping + governance behave as in prod).
180
- *
181
- * That's a near-end-to-end test of a module's runtime — schema + data port +
182
- * identity integration + engine — minus the HTTP/RSC shell (those belong to a
183
- * real-deploy smoke). The same seams back the local dev playground.
184
- *
185
- * Adding a module costs one co-located test file calling this with its own
186
- * definition + seed; there is no per-module harness to maintain.
187
- */
188
- declare function createModuleApp(options: CreateModuleAppOptions): Promise<ModuleApp>;
189
-
190
- type RouteHandler<Req extends Request, P> = (request: Req, context: {
191
- params: Promise<P>;
192
- }) => Promise<Response> | Response;
193
- interface CallRouteOptions {
194
- /** Request path, e.g. "/api/orders". Defaults to "/api/test-route". */
195
- path?: string;
196
- method?: string;
197
- /** JSON body. Implies POST unless `method` is set. */
198
- body?: unknown;
199
- searchParams?: Record<string, string>;
200
- /** Next.js dynamic segment params, e.g. { id: "123" }. */
201
- params?: Record<string, string | string[]>;
202
- headers?: Record<string, string>;
203
- cookies?: Record<string, string>;
204
- /** Override the active test user for this call only. */
205
- user?: TestUser | null;
206
- }
207
- declare function callRoute<Req extends Request = Request, P = Record<string, string | string[]>>(handler: RouteHandler<Req, P>, options?: CallRouteOptions): Promise<Response>;
208
-
209
351
  /**
210
352
  * Tags a describe block as covering a named critical workflow ("checkout",
211
353
  * "booking", "inventory"). The platform parses the `workflow:` prefix out of
@@ -219,6 +361,8 @@ declare function parseWorkflowName(describeTitle: string): string | null;
219
361
 
220
362
  declare const CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
221
363
  declare const DATA_STORE_TEST_HOST = "db.stardeck.test";
364
+ declare const STORAGE_TEST_URL = "https://storage.stardeck.test";
365
+ declare const STORAGE_TEST_HOST = "storage.stardeck.test";
222
366
  declare const TEST_ENV_DEFAULTS: {
223
367
  readonly CONTROL_PLANE_URL: "https://control-plane.stardeck.test";
224
368
  readonly DEPLOYMENT_SECRET: "stardeck-test-deployment-secret";
@@ -226,8 +370,9 @@ declare const TEST_ENV_DEFAULTS: {
226
370
  readonly PROJECT_ID: "00000000-0000-4000-8000-00000000000b";
227
371
  readonly DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c";
228
372
  readonly DATA_STORE_URL: "postgresql://test:test@db.stardeck.test/main";
373
+ readonly STORAGE_URL: "https://storage.stardeck.test";
229
374
  };
230
375
  /** Default location of the DDL snapshot written by `generate-types`. */
231
376
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
232
377
 
233
- export { CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CreateModuleAppOptions, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, type ModuleApp, type ModuleSeedDeps, type MountableModule, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestInbox, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createModuleApp, createTestApp, describeWorkflow, parseWorkflowName };
378
+ 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, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };