@stardeck-customer-apps/testing 0.6.0 → 0.6.2

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
@@ -215,10 +215,13 @@ describe the code, and the owner can't tell from them what is or isn't covered.
215
215
  - `client.identities` from integrations-sdk (create/get/update/list accounts &
216
216
  persons, attach channel links, guest/staff `resolveOrCreate`, paginated
217
217
  `search`, and batch `getAliases`) — served offline by the simulated directory.
218
- `resolveOrCreate` lowercases/trims email, requires E.164 phone values, and
218
+ `resolveOrCreate` applies the production email contract (trim + ASCII-only
219
+ A–Z→a–z; Turkish İ is left unchanged), requires E.164 phone values, and
219
220
  always writes an unverified link. A guest lookup of a verified link returns
220
221
  `{ identityId: null, reason: "verified_conflict" }`; staff provenance may
221
- resolve either verified or unverified links. `attachLink(..., { verified:
222
+ resolve either verified or unverified links. `attachLink` accepts only
223
+ channel kinds (line/facebook/instagram/email/phone); login keys must be
224
+ seeded via `app.identities.seedVerifiedLink`. `attachLink(..., { verified:
222
225
  true })` remains source-compatible but the simulator (like the control plane)
223
226
  ignores that flag and returns `verified: false`. `update` replaces the
224
227
  `profile` object (not a merge), like the control plane. Inspect via
@@ -243,6 +246,44 @@ cursor, limit })`; `list()` intentionally keeps its previous array shape.
243
246
  pairing pickers via `app.edge.seedDevices()` / `seedPeripherals()`.
244
247
  - `next/headers` (`headers()`/`cookies()`) inside handlers under `callRoute`.
245
248
 
249
+ ## End-to-end (Playwright) suite
250
+
251
+ This package covers the hermetic in-process suite. The app ALSO has a separate
252
+ Playwright e2e suite: specs in `e2e/*.spec.ts`, run with `npm run test:e2e`
253
+ against the live dev server. It is NOT hermetic — real routes, real database,
254
+ the sandbox's mock admin session.
255
+
256
+ **When to use which**
257
+
258
+ - Business logic, route handlers, edge cases, error paths → this package (fast,
259
+ isolated, deterministic).
260
+ - Whole user journeys through the real UI (the flows a customer walks) → one
261
+ e2e spec per critical workflow.
262
+
263
+ **Which flows get an e2e spec** (full guidance: `e2e/README.md`)
264
+
265
+ One spec per critical workflow — "if this broke overnight, would the owner
266
+ lose money or trust?" The flow types worth a journey: core transactions
267
+ (cart → checkout → confirmation), creation → visibility (post a review →
268
+ see it on the page), state transitions (order placed → fulfilled → status
269
+ updates), cross-surface handoffs (storefront order → admin order list). NOT
270
+ worth one: render-existence checks, admin CRUD minutiae, static pages,
271
+ per-field validation (unit-test the route), login/logout (platform-provided).
272
+
273
+ **E2e rules** (mirror `e2e/README.md`)
274
+
275
+ - Whole journeys only: start at the flow's real entry page, end by asserting
276
+ the durable user-visible outcome — never render-existence checks ("button
277
+ renders", "page loads"). One happy path per workflow first; error plumbing
278
+ stays in this package's suite.
279
+ - Group with `test.describe("workflow:<Name>")` using the same names as your
280
+ `describeWorkflow` blocks where they overlap.
281
+ - Specs create uniquely-named data INSIDE the test body (retries re-run it)
282
+ and clean up after themselves.
283
+ - Flake killers: user-facing locators (`getByRole`/`getByLabel`) over CSS,
284
+ never `waitForTimeout` (web-first assertions auto-wait), every test
285
+ independent of other tests' data and ordering.
286
+
246
287
  ## Rules
247
288
 
248
289
  - Test **business logic and route handlers**, not framework plumbing. Focus on
package/dist/index.js CHANGED
@@ -679,7 +679,8 @@ function createInbox() {
679
679
 
680
680
  // src/simulator/identities.ts
681
681
  var import_node_crypto3 = __toESM(require("crypto"));
682
- var LINK_KINDS = /* @__PURE__ */ new Set([
682
+ var ATTACH_LINK_KINDS = /* @__PURE__ */ new Set(["line", "facebook", "instagram", "email", "phone"]);
683
+ var TRUSTED_LINK_KINDS = /* @__PURE__ */ new Set([
683
684
  "line",
684
685
  "facebook",
685
686
  "instagram",
@@ -692,6 +693,15 @@ var RESOLVE_KINDS = /* @__PURE__ */ new Set(["phone", "email", "line"]);
692
693
  var E164_PHONE = /^\+[1-9]\d{1,14}$/;
693
694
  var SEARCH_DEFAULT_LIMIT = 50;
694
695
  var SEARCH_MAX_LIMIT = 100;
696
+ function asciiLowercaseTrim(value) {
697
+ const trimmed = value.trim();
698
+ let out = "";
699
+ for (let i = 0; i < trimmed.length; i++) {
700
+ const code = trimmed.charCodeAt(i);
701
+ out += code >= 65 && code <= 90 ? String.fromCharCode(code + 32) : trimmed[i];
702
+ }
703
+ return out;
704
+ }
695
705
  var IdentityGraphError = class extends Error {
696
706
  };
697
707
  function now() {
@@ -701,12 +711,12 @@ function linksFor(identityId) {
701
711
  return state.identityLinks.filter((l) => l.identityId === identityId);
702
712
  }
703
713
  function comparableExternalId(kind, externalId) {
704
- if (kind === "email") return externalId.trim().toLowerCase();
714
+ if (kind === "email") return asciiLowercaseTrim(externalId);
705
715
  return externalId;
706
716
  }
707
717
  function normalizeExternalId(kind, externalId) {
708
718
  if (kind === "email") {
709
- const normalized = externalId.trim().toLowerCase();
719
+ const normalized = asciiLowercaseTrim(externalId);
710
720
  return normalized.length > 0 ? { externalId: normalized } : { error: "externalId is required" };
711
721
  }
712
722
  if (kind === "phone" && !E164_PHONE.test(externalId)) {
@@ -870,8 +880,8 @@ async function handleAttachLink(identityId, request) {
870
880
  const normalized = normalizeLinkInput(
871
881
  body.kind,
872
882
  body.externalId,
873
- LINK_KINDS,
874
- `kind must be one of: ${[...LINK_KINDS].join(", ")}`
883
+ ATTACH_LINK_KINDS,
884
+ `kind must be one of: ${[...ATTACH_LINK_KINDS].join(", ")}`
875
885
  );
876
886
  if ("error" in normalized) return failure(normalized.error);
877
887
  const { kind, externalId } = normalized;
@@ -1046,7 +1056,7 @@ function seedVerifiedLink(identityId, params) {
1046
1056
  const normalized = normalizeLinkInput(
1047
1057
  params.kind,
1048
1058
  params.externalId,
1049
- LINK_KINDS,
1059
+ TRUSTED_LINK_KINDS,
1050
1060
  "unknown identity link kind"
1051
1061
  );
1052
1062
  if ("error" in normalized) throw new Error(normalized.error);
package/dist/index.mjs CHANGED
@@ -633,7 +633,8 @@ function createInbox() {
633
633
 
634
634
  // src/simulator/identities.ts
635
635
  import crypto3 from "crypto";
636
- var LINK_KINDS = /* @__PURE__ */ new Set([
636
+ var ATTACH_LINK_KINDS = /* @__PURE__ */ new Set(["line", "facebook", "instagram", "email", "phone"]);
637
+ var TRUSTED_LINK_KINDS = /* @__PURE__ */ new Set([
637
638
  "line",
638
639
  "facebook",
639
640
  "instagram",
@@ -646,6 +647,15 @@ var RESOLVE_KINDS = /* @__PURE__ */ new Set(["phone", "email", "line"]);
646
647
  var E164_PHONE = /^\+[1-9]\d{1,14}$/;
647
648
  var SEARCH_DEFAULT_LIMIT = 50;
648
649
  var SEARCH_MAX_LIMIT = 100;
650
+ function asciiLowercaseTrim(value) {
651
+ const trimmed = value.trim();
652
+ let out = "";
653
+ for (let i = 0; i < trimmed.length; i++) {
654
+ const code = trimmed.charCodeAt(i);
655
+ out += code >= 65 && code <= 90 ? String.fromCharCode(code + 32) : trimmed[i];
656
+ }
657
+ return out;
658
+ }
649
659
  var IdentityGraphError = class extends Error {
650
660
  };
651
661
  function now() {
@@ -655,12 +665,12 @@ function linksFor(identityId) {
655
665
  return state.identityLinks.filter((l) => l.identityId === identityId);
656
666
  }
657
667
  function comparableExternalId(kind, externalId) {
658
- if (kind === "email") return externalId.trim().toLowerCase();
668
+ if (kind === "email") return asciiLowercaseTrim(externalId);
659
669
  return externalId;
660
670
  }
661
671
  function normalizeExternalId(kind, externalId) {
662
672
  if (kind === "email") {
663
- const normalized = externalId.trim().toLowerCase();
673
+ const normalized = asciiLowercaseTrim(externalId);
664
674
  return normalized.length > 0 ? { externalId: normalized } : { error: "externalId is required" };
665
675
  }
666
676
  if (kind === "phone" && !E164_PHONE.test(externalId)) {
@@ -824,8 +834,8 @@ async function handleAttachLink(identityId, request) {
824
834
  const normalized = normalizeLinkInput(
825
835
  body.kind,
826
836
  body.externalId,
827
- LINK_KINDS,
828
- `kind must be one of: ${[...LINK_KINDS].join(", ")}`
837
+ ATTACH_LINK_KINDS,
838
+ `kind must be one of: ${[...ATTACH_LINK_KINDS].join(", ")}`
829
839
  );
830
840
  if ("error" in normalized) return failure(normalized.error);
831
841
  const { kind, externalId } = normalized;
@@ -1000,7 +1010,7 @@ function seedVerifiedLink(identityId, params) {
1000
1010
  const normalized = normalizeLinkInput(
1001
1011
  params.kind,
1002
1012
  params.externalId,
1003
- LINK_KINDS,
1013
+ TRUSTED_LINK_KINDS,
1004
1014
  "unknown identity link kind"
1005
1015
  );
1006
1016
  if ("error" in normalized) throw new Error(normalized.error);
package/dist/setup.js CHANGED
@@ -609,19 +609,20 @@ async function handleEmailSend(request) {
609
609
 
610
610
  // src/simulator/identities.ts
611
611
  var import_node_crypto3 = __toESM(require("crypto"));
612
- var LINK_KINDS = /* @__PURE__ */ new Set([
613
- "line",
614
- "facebook",
615
- "instagram",
616
- "email",
617
- "phone",
618
- "project_auth_user",
619
- "dashboard_user"
620
- ]);
612
+ var ATTACH_LINK_KINDS = /* @__PURE__ */ new Set(["line", "facebook", "instagram", "email", "phone"]);
621
613
  var RESOLVE_KINDS = /* @__PURE__ */ new Set(["phone", "email", "line"]);
622
614
  var E164_PHONE = /^\+[1-9]\d{1,14}$/;
623
615
  var SEARCH_DEFAULT_LIMIT = 50;
624
616
  var SEARCH_MAX_LIMIT = 100;
617
+ function asciiLowercaseTrim(value) {
618
+ const trimmed = value.trim();
619
+ let out = "";
620
+ for (let i = 0; i < trimmed.length; i++) {
621
+ const code = trimmed.charCodeAt(i);
622
+ out += code >= 65 && code <= 90 ? String.fromCharCode(code + 32) : trimmed[i];
623
+ }
624
+ return out;
625
+ }
625
626
  var IdentityGraphError = class extends Error {
626
627
  };
627
628
  function now() {
@@ -631,12 +632,12 @@ function linksFor(identityId) {
631
632
  return state.identityLinks.filter((l) => l.identityId === identityId);
632
633
  }
633
634
  function comparableExternalId(kind, externalId) {
634
- if (kind === "email") return externalId.trim().toLowerCase();
635
+ if (kind === "email") return asciiLowercaseTrim(externalId);
635
636
  return externalId;
636
637
  }
637
638
  function normalizeExternalId(kind, externalId) {
638
639
  if (kind === "email") {
639
- const normalized = externalId.trim().toLowerCase();
640
+ const normalized = asciiLowercaseTrim(externalId);
640
641
  return normalized.length > 0 ? { externalId: normalized } : { error: "externalId is required" };
641
642
  }
642
643
  if (kind === "phone" && !E164_PHONE.test(externalId)) {
@@ -797,8 +798,8 @@ async function handleAttachLink(identityId, request) {
797
798
  const normalized = normalizeLinkInput(
798
799
  body.kind,
799
800
  body.externalId,
800
- LINK_KINDS,
801
- `kind must be one of: ${[...LINK_KINDS].join(", ")}`
801
+ ATTACH_LINK_KINDS,
802
+ `kind must be one of: ${[...ATTACH_LINK_KINDS].join(", ")}`
802
803
  );
803
804
  if ("error" in normalized) return failure(normalized.error);
804
805
  const { kind, externalId } = normalized;
package/dist/setup.mjs CHANGED
@@ -585,19 +585,20 @@ async function handleEmailSend(request) {
585
585
 
586
586
  // src/simulator/identities.ts
587
587
  import crypto3 from "crypto";
588
- var LINK_KINDS = /* @__PURE__ */ new Set([
589
- "line",
590
- "facebook",
591
- "instagram",
592
- "email",
593
- "phone",
594
- "project_auth_user",
595
- "dashboard_user"
596
- ]);
588
+ var ATTACH_LINK_KINDS = /* @__PURE__ */ new Set(["line", "facebook", "instagram", "email", "phone"]);
597
589
  var RESOLVE_KINDS = /* @__PURE__ */ new Set(["phone", "email", "line"]);
598
590
  var E164_PHONE = /^\+[1-9]\d{1,14}$/;
599
591
  var SEARCH_DEFAULT_LIMIT = 50;
600
592
  var SEARCH_MAX_LIMIT = 100;
593
+ function asciiLowercaseTrim(value) {
594
+ const trimmed = value.trim();
595
+ let out = "";
596
+ for (let i = 0; i < trimmed.length; i++) {
597
+ const code = trimmed.charCodeAt(i);
598
+ out += code >= 65 && code <= 90 ? String.fromCharCode(code + 32) : trimmed[i];
599
+ }
600
+ return out;
601
+ }
601
602
  var IdentityGraphError = class extends Error {
602
603
  };
603
604
  function now() {
@@ -607,12 +608,12 @@ function linksFor(identityId) {
607
608
  return state.identityLinks.filter((l) => l.identityId === identityId);
608
609
  }
609
610
  function comparableExternalId(kind, externalId) {
610
- if (kind === "email") return externalId.trim().toLowerCase();
611
+ if (kind === "email") return asciiLowercaseTrim(externalId);
611
612
  return externalId;
612
613
  }
613
614
  function normalizeExternalId(kind, externalId) {
614
615
  if (kind === "email") {
615
- const normalized = externalId.trim().toLowerCase();
616
+ const normalized = asciiLowercaseTrim(externalId);
616
617
  return normalized.length > 0 ? { externalId: normalized } : { error: "externalId is required" };
617
618
  }
618
619
  if (kind === "phone" && !E164_PHONE.test(externalId)) {
@@ -773,8 +774,8 @@ async function handleAttachLink(identityId, request) {
773
774
  const normalized = normalizeLinkInput(
774
775
  body.kind,
775
776
  body.externalId,
776
- LINK_KINDS,
777
- `kind must be one of: ${[...LINK_KINDS].join(", ")}`
777
+ ATTACH_LINK_KINDS,
778
+ `kind must be one of: ${[...ATTACH_LINK_KINDS].join(", ")}`
778
779
  );
779
780
  if ("error" in normalized) return failure(normalized.error);
780
781
  const { kind, externalId } = normalized;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/testing",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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",