@stardeck-customer-apps/testing 0.10.0 → 0.12.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
@@ -203,6 +203,26 @@ const integrations = createIntegrationsClient();
203
203
  await integrations.line.push("U123", { type: "text", text: "Your order shipped" });
204
204
  expect(app.messages.channel("line").to("U123")[0].body.text).toMatch(/shipped/i);
205
205
 
206
+ // LINE Flex card — the simulator preserves the complete LINE-native container.
207
+ const card = {
208
+ type: "bubble" as const,
209
+ body: {
210
+ type: "box",
211
+ layout: "vertical",
212
+ contents: [{ type: "text", text: "Order shipped" }],
213
+ },
214
+ };
215
+ await integrations.line.push("U123", {
216
+ type: "flex",
217
+ altText: "Your order shipped",
218
+ contents: card,
219
+ });
220
+ expect(app.messages.channel("line").to("U123")[1].body).toEqual({
221
+ type: "flex",
222
+ altText: "Your order shipped",
223
+ contents: card,
224
+ });
225
+
206
226
  // Guest/counter identity resolution. `provenance` is selected by this trusted
207
227
  // server route; never copy it from a browser form or request body.
208
228
  const identity = await integrations.identities.resolveOrCreate({
@@ -265,7 +285,14 @@ expect(app.edge.latestDisplay()?.action).toBe("show");
265
285
  - `app.issueSession(partial?)` — mint access/refresh tokens for cookie-based
266
286
  auth flows (`__stardeck:sess`).
267
287
  - `app.inbox` — emails sent through the email-sdk: `.latest(addr?)`,
268
- `.to(addr)`, `.all()`, `.count`, `.clear()`.
288
+ `.to(addr)`, `.all()`, `.count`, `.clear()`. Each captured email carries
289
+ `headers` (`{}` when none were set), so a reply that should thread onto an
290
+ inbound conversation can be asserted:
291
+
292
+ ```ts
293
+ expect(app.inbox.latest()?.headers["In-Reply-To"]).toBe("<original@mail.example>");
294
+ ```
295
+
269
296
  - `app.identities` — the platform-identity directory created through
270
297
  integrations-sdk `client.identities`: `.get(id)`, `.links(id)`, `.all()`,
271
298
  `.count`, `.clear()`. The test-only `.seedVerifiedLink(id, { kind,
@@ -288,7 +315,10 @@ externalId })` helper models a trusted platform/channel link, and
288
315
  list `limit` 1–100, status filter values).
289
316
  - `app.storage` — uploads through storage-sdk: `.uploads`, `.latest()`, `.count`, `.clear()`.
290
317
  - `app.messages` — outbound Slack/LINE/Facebook sends:
291
- `.all()`, `.latest()`, `.to(recipient)`, `.channel("line")`, `.count`, `.clear()`.
318
+ `.all()`, `.latest()`, `.to(recipient)`, `.channel("line")`, `.count`, `.clear()`. LINE Flex
319
+ captures expose `body.type`, `body.altText`, and the complete `body.contents` bubble/carousel.
320
+ The simulator validates the Flex envelope like production; LINE itself remains responsible for
321
+ validating nested component layout.
292
322
  - `app.edge` — edge print/display/bindings from edge-sdk: `.prints`, `.displays`,
293
323
  `.testPrints`, `.latestPrint()`, `.latestDisplay()`, `.bindings`,
294
324
  `.seedDevices()`, `.seedPeripherals()`, `.seedBindings()`,
package/dist/index.d.mts CHANGED
@@ -53,6 +53,11 @@ interface CapturedEmail {
53
53
  html?: string;
54
54
  text?: string;
55
55
  replyTo?: string;
56
+ /**
57
+ * Raw headers the sender attached, e.g. the `In-Reply-To` / `References` pair
58
+ * that threads a reply onto an inbound conversation. Empty when none were set.
59
+ */
60
+ headers: Record<string, string>;
56
61
  attachments: Array<{
57
62
  filename: string;
58
63
  contentType?: string;
@@ -252,6 +257,7 @@ interface CapturedMessage {
252
257
  channel: "slack" | "line" | "facebook";
253
258
  recipient: string;
254
259
  body: {
260
+ type?: string;
255
261
  text?: string;
256
262
  blocks?: unknown[];
257
263
  threadTs?: string;
@@ -261,6 +267,10 @@ interface CapturedMessage {
261
267
  originalContentUrl?: string;
262
268
  /** LINE image push — HTTPS URL of the preview; may be omitted when not sent. */
263
269
  previewImageUrl?: string;
270
+ /** LINE Flex fallback text shown when the rich card cannot be rendered. */
271
+ altText?: string;
272
+ /** Complete LINE Flex bubble or carousel payload. */
273
+ contents?: Record<string, unknown>;
264
274
  };
265
275
  connectionId?: string;
266
276
  sentAt: Date;
package/dist/index.d.ts CHANGED
@@ -53,6 +53,11 @@ interface CapturedEmail {
53
53
  html?: string;
54
54
  text?: string;
55
55
  replyTo?: string;
56
+ /**
57
+ * Raw headers the sender attached, e.g. the `In-Reply-To` / `References` pair
58
+ * that threads a reply onto an inbound conversation. Empty when none were set.
59
+ */
60
+ headers: Record<string, string>;
56
61
  attachments: Array<{
57
62
  filename: string;
58
63
  contentType?: string;
@@ -252,6 +257,7 @@ interface CapturedMessage {
252
257
  channel: "slack" | "line" | "facebook";
253
258
  recipient: string;
254
259
  body: {
260
+ type?: string;
255
261
  text?: string;
256
262
  blocks?: unknown[];
257
263
  threadTs?: string;
@@ -261,6 +267,10 @@ interface CapturedMessage {
261
267
  originalContentUrl?: string;
262
268
  /** LINE image push — HTTPS URL of the preview; may be omitted when not sent. */
263
269
  previewImageUrl?: string;
270
+ /** LINE Flex fallback text shown when the rich card cannot be rendered. */
271
+ altText?: string;
272
+ /** Complete LINE Flex bubble or carousel payload. */
273
+ contents?: Record<string, unknown>;
264
274
  };
265
275
  connectionId?: string;
266
276
  sentAt: Date;
package/dist/index.js CHANGED
@@ -660,6 +660,7 @@ async function handleEmailSend(request) {
660
660
  html: body.html ? String(body.html) : void 0,
661
661
  text: body.text ? String(body.text) : void 0,
662
662
  replyTo: body.replyTo ? String(body.replyTo) : void 0,
663
+ headers: { ...body.headers ?? {} },
663
664
  attachments: (body.attachments ?? []).map((a) => ({
664
665
  filename: String(a.filename ?? ""),
665
666
  contentType: a.contentType ? String(a.contentType) : void 0
@@ -2072,6 +2073,9 @@ function isValidLineImageUrl(url) {
2072
2073
  return false;
2073
2074
  }
2074
2075
  }
2076
+ function isObject(value) {
2077
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2078
+ }
2075
2079
  function captureMessage(channel, recipient, body, connectionId) {
2076
2080
  const message = {
2077
2081
  channel,
@@ -2083,7 +2087,8 @@ function captureMessage(channel, recipient, body, connectionId) {
2083
2087
  messagingType: body.messagingType ? String(body.messagingType) : void 0,
2084
2088
  tag: body.tag ? String(body.tag) : void 0,
2085
2089
  originalContentUrl: body.originalContentUrl ? String(body.originalContentUrl) : void 0,
2086
- previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0
2090
+ previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0,
2091
+ ...body.type === "flex" && isObject(body.contents) ? { type: "flex", altText: String(body.altText), contents: body.contents } : {}
2087
2092
  },
2088
2093
  connectionId,
2089
2094
  sentAt: /* @__PURE__ */ new Date()
@@ -2121,8 +2126,8 @@ async function handleMessagingRequest(request, channel, subPath) {
2121
2126
  const userId = String(body.userId ?? "");
2122
2127
  const message = body.message;
2123
2128
  if (!userId) return failure("userId is required");
2124
- if (!message || message.type !== "text" && message.type !== "image") {
2125
- return failure("message must be { type: 'text' | 'image', ... }");
2129
+ if (!message || message.type !== "text" && message.type !== "image" && message.type !== "flex") {
2130
+ return failure("message must be { type: 'text' | 'image' | 'flex', ... }");
2126
2131
  }
2127
2132
  const connectionId = body.connectionId ? String(body.connectionId) : void 0;
2128
2133
  if (message.type === "text") {
@@ -2133,7 +2138,7 @@ async function handleMessagingRequest(request, channel, subPath) {
2133
2138
  return failure("message text exceeds maximum length");
2134
2139
  }
2135
2140
  captureMessage("line", userId, { text: message.text }, connectionId);
2136
- } else {
2141
+ } else if (message.type === "image") {
2137
2142
  const originalContentUrl = message.originalContentUrl;
2138
2143
  if (!originalContentUrl || !isValidLineImageUrl(originalContentUrl)) {
2139
2144
  return failure("originalContentUrl must be a valid HTTPS URL (max 2000 chars)");
@@ -2144,7 +2149,23 @@ async function handleMessagingRequest(request, channel, subPath) {
2144
2149
  captureMessage(
2145
2150
  "line",
2146
2151
  userId,
2147
- { originalContentUrl, previewImageUrl: message.previewImageUrl ?? originalContentUrl },
2152
+ {
2153
+ originalContentUrl,
2154
+ previewImageUrl: message.previewImageUrl ?? originalContentUrl
2155
+ },
2156
+ connectionId
2157
+ );
2158
+ } else {
2159
+ if (typeof message.altText !== "string" || !message.altText || message.altText.length > 400) {
2160
+ return failure("altText must be between 1 and 400 characters");
2161
+ }
2162
+ if (!isObject(message.contents) || message.contents.type !== "bubble" && message.contents.type !== "carousel") {
2163
+ return failure("contents.type must be 'bubble' or 'carousel'");
2164
+ }
2165
+ captureMessage(
2166
+ "line",
2167
+ userId,
2168
+ { type: "flex", altText: message.altText, contents: message.contents },
2148
2169
  connectionId
2149
2170
  );
2150
2171
  }
package/dist/index.mjs CHANGED
@@ -611,6 +611,7 @@ async function handleEmailSend(request) {
611
611
  html: body.html ? String(body.html) : void 0,
612
612
  text: body.text ? String(body.text) : void 0,
613
613
  replyTo: body.replyTo ? String(body.replyTo) : void 0,
614
+ headers: { ...body.headers ?? {} },
614
615
  attachments: (body.attachments ?? []).map((a) => ({
615
616
  filename: String(a.filename ?? ""),
616
617
  contentType: a.contentType ? String(a.contentType) : void 0
@@ -2023,6 +2024,9 @@ function isValidLineImageUrl(url) {
2023
2024
  return false;
2024
2025
  }
2025
2026
  }
2027
+ function isObject(value) {
2028
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2029
+ }
2026
2030
  function captureMessage(channel, recipient, body, connectionId) {
2027
2031
  const message = {
2028
2032
  channel,
@@ -2034,7 +2038,8 @@ function captureMessage(channel, recipient, body, connectionId) {
2034
2038
  messagingType: body.messagingType ? String(body.messagingType) : void 0,
2035
2039
  tag: body.tag ? String(body.tag) : void 0,
2036
2040
  originalContentUrl: body.originalContentUrl ? String(body.originalContentUrl) : void 0,
2037
- previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0
2041
+ previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0,
2042
+ ...body.type === "flex" && isObject(body.contents) ? { type: "flex", altText: String(body.altText), contents: body.contents } : {}
2038
2043
  },
2039
2044
  connectionId,
2040
2045
  sentAt: /* @__PURE__ */ new Date()
@@ -2072,8 +2077,8 @@ async function handleMessagingRequest(request, channel, subPath) {
2072
2077
  const userId = String(body.userId ?? "");
2073
2078
  const message = body.message;
2074
2079
  if (!userId) return failure("userId is required");
2075
- if (!message || message.type !== "text" && message.type !== "image") {
2076
- return failure("message must be { type: 'text' | 'image', ... }");
2080
+ if (!message || message.type !== "text" && message.type !== "image" && message.type !== "flex") {
2081
+ return failure("message must be { type: 'text' | 'image' | 'flex', ... }");
2077
2082
  }
2078
2083
  const connectionId = body.connectionId ? String(body.connectionId) : void 0;
2079
2084
  if (message.type === "text") {
@@ -2084,7 +2089,7 @@ async function handleMessagingRequest(request, channel, subPath) {
2084
2089
  return failure("message text exceeds maximum length");
2085
2090
  }
2086
2091
  captureMessage("line", userId, { text: message.text }, connectionId);
2087
- } else {
2092
+ } else if (message.type === "image") {
2088
2093
  const originalContentUrl = message.originalContentUrl;
2089
2094
  if (!originalContentUrl || !isValidLineImageUrl(originalContentUrl)) {
2090
2095
  return failure("originalContentUrl must be a valid HTTPS URL (max 2000 chars)");
@@ -2095,7 +2100,23 @@ async function handleMessagingRequest(request, channel, subPath) {
2095
2100
  captureMessage(
2096
2101
  "line",
2097
2102
  userId,
2098
- { originalContentUrl, previewImageUrl: message.previewImageUrl ?? originalContentUrl },
2103
+ {
2104
+ originalContentUrl,
2105
+ previewImageUrl: message.previewImageUrl ?? originalContentUrl
2106
+ },
2107
+ connectionId
2108
+ );
2109
+ } else {
2110
+ if (typeof message.altText !== "string" || !message.altText || message.altText.length > 400) {
2111
+ return failure("altText must be between 1 and 400 characters");
2112
+ }
2113
+ if (!isObject(message.contents) || message.contents.type !== "bubble" && message.contents.type !== "carousel") {
2114
+ return failure("contents.type must be 'bubble' or 'carousel'");
2115
+ }
2116
+ captureMessage(
2117
+ "line",
2118
+ userId,
2119
+ { type: "flex", altText: message.altText, contents: message.contents },
2099
2120
  connectionId
2100
2121
  );
2101
2122
  }
package/dist/setup.js CHANGED
@@ -607,6 +607,7 @@ async function handleEmailSend(request) {
607
607
  html: body.html ? String(body.html) : void 0,
608
608
  text: body.text ? String(body.text) : void 0,
609
609
  replyTo: body.replyTo ? String(body.replyTo) : void 0,
610
+ headers: { ...body.headers ?? {} },
610
611
  attachments: (body.attachments ?? []).map((a) => ({
611
612
  filename: String(a.filename ?? ""),
612
613
  contentType: a.contentType ? String(a.contentType) : void 0
@@ -1622,6 +1623,9 @@ function isValidLineImageUrl(url) {
1622
1623
  return false;
1623
1624
  }
1624
1625
  }
1626
+ function isObject(value) {
1627
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1628
+ }
1625
1629
  function captureMessage(channel, recipient, body, connectionId) {
1626
1630
  const message = {
1627
1631
  channel,
@@ -1633,7 +1637,8 @@ function captureMessage(channel, recipient, body, connectionId) {
1633
1637
  messagingType: body.messagingType ? String(body.messagingType) : void 0,
1634
1638
  tag: body.tag ? String(body.tag) : void 0,
1635
1639
  originalContentUrl: body.originalContentUrl ? String(body.originalContentUrl) : void 0,
1636
- previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0
1640
+ previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0,
1641
+ ...body.type === "flex" && isObject(body.contents) ? { type: "flex", altText: String(body.altText), contents: body.contents } : {}
1637
1642
  },
1638
1643
  connectionId,
1639
1644
  sentAt: /* @__PURE__ */ new Date()
@@ -1671,8 +1676,8 @@ async function handleMessagingRequest(request, channel, subPath) {
1671
1676
  const userId = String(body.userId ?? "");
1672
1677
  const message = body.message;
1673
1678
  if (!userId) return failure("userId is required");
1674
- if (!message || message.type !== "text" && message.type !== "image") {
1675
- return failure("message must be { type: 'text' | 'image', ... }");
1679
+ if (!message || message.type !== "text" && message.type !== "image" && message.type !== "flex") {
1680
+ return failure("message must be { type: 'text' | 'image' | 'flex', ... }");
1676
1681
  }
1677
1682
  const connectionId = body.connectionId ? String(body.connectionId) : void 0;
1678
1683
  if (message.type === "text") {
@@ -1683,7 +1688,7 @@ async function handleMessagingRequest(request, channel, subPath) {
1683
1688
  return failure("message text exceeds maximum length");
1684
1689
  }
1685
1690
  captureMessage("line", userId, { text: message.text }, connectionId);
1686
- } else {
1691
+ } else if (message.type === "image") {
1687
1692
  const originalContentUrl = message.originalContentUrl;
1688
1693
  if (!originalContentUrl || !isValidLineImageUrl(originalContentUrl)) {
1689
1694
  return failure("originalContentUrl must be a valid HTTPS URL (max 2000 chars)");
@@ -1694,7 +1699,23 @@ async function handleMessagingRequest(request, channel, subPath) {
1694
1699
  captureMessage(
1695
1700
  "line",
1696
1701
  userId,
1697
- { originalContentUrl, previewImageUrl: message.previewImageUrl ?? originalContentUrl },
1702
+ {
1703
+ originalContentUrl,
1704
+ previewImageUrl: message.previewImageUrl ?? originalContentUrl
1705
+ },
1706
+ connectionId
1707
+ );
1708
+ } else {
1709
+ if (typeof message.altText !== "string" || !message.altText || message.altText.length > 400) {
1710
+ return failure("altText must be between 1 and 400 characters");
1711
+ }
1712
+ if (!isObject(message.contents) || message.contents.type !== "bubble" && message.contents.type !== "carousel") {
1713
+ return failure("contents.type must be 'bubble' or 'carousel'");
1714
+ }
1715
+ captureMessage(
1716
+ "line",
1717
+ userId,
1718
+ { type: "flex", altText: message.altText, contents: message.contents },
1698
1719
  connectionId
1699
1720
  );
1700
1721
  }
package/dist/setup.mjs CHANGED
@@ -583,6 +583,7 @@ async function handleEmailSend(request) {
583
583
  html: body.html ? String(body.html) : void 0,
584
584
  text: body.text ? String(body.text) : void 0,
585
585
  replyTo: body.replyTo ? String(body.replyTo) : void 0,
586
+ headers: { ...body.headers ?? {} },
586
587
  attachments: (body.attachments ?? []).map((a) => ({
587
588
  filename: String(a.filename ?? ""),
588
589
  contentType: a.contentType ? String(a.contentType) : void 0
@@ -1598,6 +1599,9 @@ function isValidLineImageUrl(url) {
1598
1599
  return false;
1599
1600
  }
1600
1601
  }
1602
+ function isObject(value) {
1603
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1604
+ }
1601
1605
  function captureMessage(channel, recipient, body, connectionId) {
1602
1606
  const message = {
1603
1607
  channel,
@@ -1609,7 +1613,8 @@ function captureMessage(channel, recipient, body, connectionId) {
1609
1613
  messagingType: body.messagingType ? String(body.messagingType) : void 0,
1610
1614
  tag: body.tag ? String(body.tag) : void 0,
1611
1615
  originalContentUrl: body.originalContentUrl ? String(body.originalContentUrl) : void 0,
1612
- previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0
1616
+ previewImageUrl: body.previewImageUrl ? String(body.previewImageUrl) : void 0,
1617
+ ...body.type === "flex" && isObject(body.contents) ? { type: "flex", altText: String(body.altText), contents: body.contents } : {}
1613
1618
  },
1614
1619
  connectionId,
1615
1620
  sentAt: /* @__PURE__ */ new Date()
@@ -1647,8 +1652,8 @@ async function handleMessagingRequest(request, channel, subPath) {
1647
1652
  const userId = String(body.userId ?? "");
1648
1653
  const message = body.message;
1649
1654
  if (!userId) return failure("userId is required");
1650
- if (!message || message.type !== "text" && message.type !== "image") {
1651
- return failure("message must be { type: 'text' | 'image', ... }");
1655
+ if (!message || message.type !== "text" && message.type !== "image" && message.type !== "flex") {
1656
+ return failure("message must be { type: 'text' | 'image' | 'flex', ... }");
1652
1657
  }
1653
1658
  const connectionId = body.connectionId ? String(body.connectionId) : void 0;
1654
1659
  if (message.type === "text") {
@@ -1659,7 +1664,7 @@ async function handleMessagingRequest(request, channel, subPath) {
1659
1664
  return failure("message text exceeds maximum length");
1660
1665
  }
1661
1666
  captureMessage("line", userId, { text: message.text }, connectionId);
1662
- } else {
1667
+ } else if (message.type === "image") {
1663
1668
  const originalContentUrl = message.originalContentUrl;
1664
1669
  if (!originalContentUrl || !isValidLineImageUrl(originalContentUrl)) {
1665
1670
  return failure("originalContentUrl must be a valid HTTPS URL (max 2000 chars)");
@@ -1670,7 +1675,23 @@ async function handleMessagingRequest(request, channel, subPath) {
1670
1675
  captureMessage(
1671
1676
  "line",
1672
1677
  userId,
1673
- { originalContentUrl, previewImageUrl: message.previewImageUrl ?? originalContentUrl },
1678
+ {
1679
+ originalContentUrl,
1680
+ previewImageUrl: message.previewImageUrl ?? originalContentUrl
1681
+ },
1682
+ connectionId
1683
+ );
1684
+ } else {
1685
+ if (typeof message.altText !== "string" || !message.altText || message.altText.length > 400) {
1686
+ return failure("altText must be between 1 and 400 characters");
1687
+ }
1688
+ if (!isObject(message.contents) || message.contents.type !== "bubble" && message.contents.type !== "carousel") {
1689
+ return failure("contents.type must be 'bubble' or 'carousel'");
1690
+ }
1691
+ captureMessage(
1692
+ "line",
1693
+ userId,
1694
+ { type: "flex", altText: message.altText, contents: message.contents },
1674
1695
  connectionId
1675
1696
  );
1676
1697
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/testing",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "peerDependencies": {
68
68
  "@stardeck-customer-apps/edge-sdk": ">=1.4.0",
69
- "@stardeck-customer-apps/integrations-sdk": ">=1.6.0",
69
+ "@stardeck-customer-apps/integrations-sdk": ">=1.13.0",
70
70
  "@stardeck-customer-apps/payments-sdk": ">=0.1.0",
71
71
  "@stardeck-customer-apps/storage-sdk": ">=0.1.0",
72
72
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",