@stardeck-customer-apps/testing 0.5.0 → 0.6.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
@@ -2,8 +2,9 @@
2
2
 
3
3
  Vitest harness for Stardeck apps. Tests run against an **in-process Postgres
4
4
  (PGlite)** and a **simulated control plane**, so the real Stardeck SDKs
5
- (`data-store-sdk`, `email-sdk`, `project-auth`) execute their production code
6
- paths with no network, no mocks to write, and full determinism.
5
+ (`data-store-sdk`, `email-sdk`, `project-auth`, and `integrations-sdk`) execute
6
+ their production code paths with no network, no mocks to write, and full
7
+ determinism.
7
8
 
8
9
  ## Setup (once per project)
9
10
 
@@ -53,7 +54,7 @@ beforeEach(() => app.reset());
53
54
  afterAll(() => app.close());
54
55
 
55
56
  describeWorkflow("checkout", () => {
56
- it("completes an order, decrements stock, and emails the customer", async () => {
57
+ it("someone buys a widget and gets a confirmation email", async () => {
57
58
  const user = app.asUser({ email: "buyer@example.com" });
58
59
 
59
60
  const res = await callRoute(checkout, { body: { items: [{ sku: "Widget", qty: 2 }] } });
@@ -102,6 +103,34 @@ const integrations = createIntegrationsClient();
102
103
  await integrations.line.push("U123", { type: "text", text: "Your order shipped" });
103
104
  expect(app.messages.channel("line").to("U123")[0].body.text).toMatch(/shipped/i);
104
105
 
106
+ // Guest/counter identity resolution. `provenance` is selected by this trusted
107
+ // server route; never copy it from a browser form or request body.
108
+ const identity = await integrations.identities.resolveOrCreate({
109
+ kind: "email",
110
+ externalId: "guest@example.com",
111
+ provenance: "guest",
112
+ displayName: "Guest",
113
+ });
114
+ if (identity.identityId) {
115
+ const identityId = identity.identityId;
116
+ const aliases = await integrations.identities.getAliases(identityId);
117
+ const aliasSet = Object.values(aliases).find((ids) => ids.includes(identityId)) ?? [identityId];
118
+ expect(aliasSet).toContain(identityId);
119
+ }
120
+
121
+ // A staff POS route derives `staff` only after checking an authenticated staff
122
+ // session and permission. A guest lookup against a verified link returns the
123
+ // safe conflict branch instead of attributing activity to that customer.
124
+ const staffIdentity = await integrations.identities.resolveOrCreate({
125
+ kind: "phone",
126
+ externalId: "+15550100",
127
+ provenance: "staff",
128
+ });
129
+ if (staffIdentity.identityId) {
130
+ const page = await integrations.identities.search({ query: "15550100", limit: 10 });
131
+ expect(page.items.map((item) => item.id)).toContain(staffIdentity.identityId);
132
+ }
133
+
105
134
  // Receipt print + display
106
135
  import { createEdgeClient } from "@stardeck-customer-apps/edge-sdk/server";
107
136
 
@@ -132,7 +161,11 @@ expect(app.edge.latestDisplay()?.action).toBe("show");
132
161
  `.to(addr)`, `.all()`, `.count`, `.clear()`.
133
162
  - `app.identities` — the platform-identity directory created through
134
163
  integrations-sdk `client.identities`: `.get(id)`, `.links(id)`, `.all()`,
135
- `.count`, `.clear()`.
164
+ `.count`, `.clear()`. The test-only `.seedVerifiedLink(id, { kind,
165
+ externalId })` helper models a trusted platform/channel link, and
166
+ `.merge(sourceId, canonicalId)` models governed merge redirects. They are
167
+ setup helpers only; deployed app code cannot create verified links or merge
168
+ identities through the integrations SDK.
136
169
  - `app.payments` — checkouts created through payments-sdk:
137
170
  `.checkouts`, `.latest()`, `.setProducts()`, `.markPaid(id)`,
138
171
  `.deliverStripeEvent(handler, event)`, `.deliverBeamEvent(handler, event)`,
@@ -153,6 +186,23 @@ expect(app.edge.latestDisplay()?.action).toBe("show");
153
186
  ("checkout", "booking", "inventory"). The platform reports these per
154
187
  deployment — every critical workflow should have one.
155
188
 
189
+ ### Naming
190
+
191
+ The app owner reads these names in the dashboard, under the workflow, without
192
+ ever opening the code. Name the workflow after the business capability, and name
193
+ each test after the thing a person does, in a full sentence:
194
+
195
+ ```ts
196
+ describeWorkflow("checkout", () => {
197
+ it("someone adds an item to their cart", ...);
198
+ it("someone pays for their cart with a credit card", ...);
199
+ it("someone whose card is declined keeps their cart", ...);
200
+ });
201
+ ```
202
+
203
+ Not `it("POST /api/checkout returns 200")` or `it("decrements stock")` — those
204
+ describe the code, and the owner can't tell from them what is or isn't covered.
205
+
156
206
  ## What works out of the box
157
207
 
158
208
  - `DataStoreClient` (query/insert/update/delete/schema ops) — served by the
@@ -163,9 +213,24 @@ expect(app.edge.latestDisplay()?.action).toBe("show");
163
213
  `asUser(...)` user (header fast path) or issued session cookies.
164
214
  - `EmailClient.send()` — captured in `app.inbox`, never delivered.
165
215
  - `client.identities` from integrations-sdk (create/get/update/list accounts &
166
- persons, attach channel links) served offline by the simulated directory;
167
- `update` replaces the `profile` object (not a merge), like the control plane.
168
- Inspect via `app.identities`. Merge/archive are dashboard-only not simulated.
216
+ persons, attach channel links, guest/staff `resolveOrCreate`, paginated
217
+ `search`, and batch `getAliases`) served offline by the simulated directory.
218
+ `resolveOrCreate` lowercases/trims email, requires E.164 phone values, and
219
+ always writes an unverified link. A guest lookup of a verified link returns
220
+ `{ identityId: null, reason: "verified_conflict" }`; staff provenance may
221
+ resolve either verified or unverified links. `attachLink(..., { verified:
222
+ true })` remains source-compatible but the simulator (like the control plane)
223
+ ignores that flag and returns `verified: false`. `update` replaces the
224
+ `profile` object (not a merge), like the control plane. Inspect via
225
+ `app.identities`; merge/archive are privileged setup operations, with only
226
+ test-only merge modeling exposed above.
227
+ - `session.user.identityId` is the platform-provisioned cross-app customer key.
228
+ `session.user.id` is the platform login key; it is not the key for customer
229
+ data. The simulator round-trips `identityId` through issued sessions while
230
+ preserving older users that omit it or set it to `null`.
231
+ - For merge-correct reads, request aliases in batches and include every id in
232
+ the returned canonical set. Page directory UIs through `search({ query,
233
+ cursor, limit })`; `list()` intentionally keeps its previous array shape.
169
234
  - `PaymentsServerClient` (Stripe checkout + Beam payment links, product list) —
170
235
  captured in `app.payments`; fulfill via `markPaid` (poll) or
171
236
  `deliverStripeEvent` / `deliverBeamEvent` (webhook push).
package/dist/index.d.mts CHANGED
@@ -34,6 +34,8 @@ interface TestUser {
34
34
  permissions?: string[];
35
35
  organizationId?: string | null;
36
36
  projectId?: string | null;
37
+ /** Cross-app customer ownership key returned by the platform identity resolver. */
38
+ identityId?: string | null;
37
39
  }
38
40
  interface CapturedEmail {
39
41
  resendId: string;
@@ -103,6 +105,20 @@ interface TestDirectory {
103
105
  get(id: string): CapturedIdentity | undefined;
104
106
  /** Channel/login links attached to an identity. */
105
107
  links(identityId: string): CapturedIdentityLink[];
108
+ /**
109
+ * Test setup only: seed a platform-verified link. Deployment SDK writes are
110
+ * intentionally unverified; this helper models a trusted control-plane or
111
+ * channel source so guest conflict and adoption tests can be deterministic.
112
+ */
113
+ seedVerifiedLink(identityId: string, params: {
114
+ kind: string;
115
+ externalId: string;
116
+ }): CapturedIdentityLink;
117
+ /**
118
+ * Test setup only: govern a merge from one identity into an active canonical
119
+ * identity. The production integrations SDK does not expose merge operations.
120
+ */
121
+ merge(sourceIdentityId: string, canonicalIdentityId: string): void;
106
122
  clear(): void;
107
123
  get count(): number;
108
124
  }
@@ -353,6 +369,10 @@ declare function createTestApp(options?: TestAppOptions): Promise<TestApp>;
353
369
  * "booking", "inventory"). The platform parses the `workflow:` prefix out of
354
370
  * test reporter output to show per-workflow pass/fail on deployments — use
355
371
  * one block per workflow the app's owner cares about.
372
+ *
373
+ * The dashboard lists the workflow's test names beneath it, so title each test
374
+ * as a sentence about what a person does ("someone pays with a credit card"),
375
+ * not about the code under it.
356
376
  */
357
377
  declare function describeWorkflow(name: string, fn: () => void): void;
358
378
  declare const WORKFLOW_NAME_PREFIX = "workflow:";
package/dist/index.d.ts CHANGED
@@ -34,6 +34,8 @@ interface TestUser {
34
34
  permissions?: string[];
35
35
  organizationId?: string | null;
36
36
  projectId?: string | null;
37
+ /** Cross-app customer ownership key returned by the platform identity resolver. */
38
+ identityId?: string | null;
37
39
  }
38
40
  interface CapturedEmail {
39
41
  resendId: string;
@@ -103,6 +105,20 @@ interface TestDirectory {
103
105
  get(id: string): CapturedIdentity | undefined;
104
106
  /** Channel/login links attached to an identity. */
105
107
  links(identityId: string): CapturedIdentityLink[];
108
+ /**
109
+ * Test setup only: seed a platform-verified link. Deployment SDK writes are
110
+ * intentionally unverified; this helper models a trusted control-plane or
111
+ * channel source so guest conflict and adoption tests can be deterministic.
112
+ */
113
+ seedVerifiedLink(identityId: string, params: {
114
+ kind: string;
115
+ externalId: string;
116
+ }): CapturedIdentityLink;
117
+ /**
118
+ * Test setup only: govern a merge from one identity into an active canonical
119
+ * identity. The production integrations SDK does not expose merge operations.
120
+ */
121
+ merge(sourceIdentityId: string, canonicalIdentityId: string): void;
106
122
  clear(): void;
107
123
  get count(): number;
108
124
  }
@@ -353,6 +369,10 @@ declare function createTestApp(options?: TestAppOptions): Promise<TestApp>;
353
369
  * "booking", "inventory"). The platform parses the `workflow:` prefix out of
354
370
  * test reporter output to show per-workflow pass/fail on deployments — use
355
371
  * one block per workflow the app's owner cares about.
372
+ *
373
+ * The dashboard lists the workflow's test names beneath it, so title each test
374
+ * as a sentence about what a person does ("someone pays with a credit card"),
375
+ * not about the code under it.
356
376
  */
357
377
  declare function describeWorkflow(name: string, fn: () => void): void;
358
378
  declare const WORKFLOW_NAME_PREFIX = "workflow:";