@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 +72 -7
- package/dist/index.d.mts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +362 -29
- package/dist/index.mjs +362 -29
- package/dist/setup.js +283 -29
- package/dist/setup.mjs +283 -29
- package/package.json +1 -1
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
|
|
6
|
-
paths with no network, no mocks to write, and full
|
|
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("
|
|
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
|
|
167
|
-
`
|
|
168
|
-
|
|
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:";
|