@tappify/extension-sdk 2.1.0-beta.1 → 2.1.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.
Files changed (137) hide show
  1. package/README.md +18 -54
  2. package/dist/{chunk-6HETNZJY.cjs → chunk-2DBSVKAU.cjs} +1 -1
  3. package/dist/{chunk-6HETNZJY.cjs.map → chunk-2DBSVKAU.cjs.map} +1 -1
  4. package/dist/{chunk-O357OJXL.cjs → chunk-3YWVYA42.cjs} +18 -14
  5. package/dist/chunk-3YWVYA42.cjs.map +1 -0
  6. package/dist/{chunk-GAUZYBXE.cjs → chunk-5AAPD5WV.cjs} +11 -11
  7. package/dist/chunk-5AAPD5WV.cjs.map +1 -0
  8. package/dist/{chunk-SGEYDV3K.mjs → chunk-BDCX6LRQ.mjs} +3 -3
  9. package/dist/chunk-BDCX6LRQ.mjs.map +1 -0
  10. package/dist/{chunk-KZ5NGFUP.mjs → chunk-C3YNVHBP.mjs} +7 -20
  11. package/dist/chunk-C3YNVHBP.mjs.map +1 -0
  12. package/dist/{chunk-QXD7WVKK.mjs → chunk-CSTYBKZC.mjs} +1 -1
  13. package/dist/chunk-CSTYBKZC.mjs.map +1 -0
  14. package/dist/{chunk-SWG7CNTZ.mjs → chunk-CTSZ5D7S.mjs} +5 -9
  15. package/dist/chunk-CTSZ5D7S.mjs.map +1 -0
  16. package/dist/chunk-EW54H7CR.cjs +722 -0
  17. package/dist/chunk-EW54H7CR.cjs.map +1 -0
  18. package/dist/chunk-EX3NFXDK.mjs +722 -0
  19. package/dist/chunk-EX3NFXDK.mjs.map +1 -0
  20. package/dist/{chunk-A74HCO6T.cjs → chunk-G7XZSN2Q.cjs} +18 -31
  21. package/dist/chunk-G7XZSN2Q.cjs.map +1 -0
  22. package/dist/{chunk-RBIQT75L.mjs → chunk-GG3IX52W.mjs} +2 -2
  23. package/dist/{chunk-SLGOHIBL.mjs → chunk-IGK2AP6J.mjs} +17 -13
  24. package/dist/chunk-IGK2AP6J.mjs.map +1 -0
  25. package/dist/{chunk-VV7ELNRW.cjs → chunk-JMUK3K47.cjs} +3 -3
  26. package/dist/{chunk-VV7ELNRW.cjs.map → chunk-JMUK3K47.cjs.map} +1 -1
  27. package/dist/{chunk-SGCYEEXD.mjs → chunk-MNDVYPEP.mjs} +2 -9
  28. package/dist/chunk-MNDVYPEP.mjs.map +1 -0
  29. package/dist/{chunk-BPCGXXMD.cjs → chunk-NFUPPCX4.cjs} +18 -22
  30. package/dist/chunk-NFUPPCX4.cjs.map +1 -0
  31. package/dist/{chunk-PRCZMOLD.cjs → chunk-OPPYEA7F.cjs} +3 -3
  32. package/dist/{chunk-PRCZMOLD.cjs.map → chunk-OPPYEA7F.cjs.map} +1 -1
  33. package/dist/{chunk-7YKH4LZN.cjs → chunk-PYJMJF44.cjs} +2 -9
  34. package/dist/chunk-PYJMJF44.cjs.map +1 -0
  35. package/dist/{chunk-QA5AL3VZ.mjs → chunk-QHM7XNRK.mjs} +2 -2
  36. package/dist/{chunk-QA5AL3VZ.mjs.map → chunk-QHM7XNRK.mjs.map} +1 -1
  37. package/dist/codegen.cjs.map +1 -1
  38. package/dist/codegen.d.cts +7 -40
  39. package/dist/codegen.d.ts +7 -40
  40. package/dist/codegen.mjs.map +1 -1
  41. package/dist/{constants-BAimnqOK.d.cts → constants-DJ_KgZpN.d.cts} +8 -41
  42. package/dist/{constants-BAimnqOK.d.ts → constants-DJ_KgZpN.d.ts} +8 -41
  43. package/dist/{context-Cr93B05o.d.ts → context-Dt6xh_5t.d.cts} +4 -5
  44. package/dist/{context-DJ3REtdT.d.cts → context-Ml7k_V14.d.ts} +4 -5
  45. package/dist/{errors-Cqh0wrqt.d.ts → errors-COsBh7kd.d.cts} +2 -3
  46. package/dist/{errors-Cqh0wrqt.d.cts → errors-COsBh7kd.d.ts} +2 -3
  47. package/dist/handler-CZIlZ5vu.d.ts +227 -0
  48. package/dist/handler-kU9Jf36K.d.cts +227 -0
  49. package/dist/host.cjs +4 -4
  50. package/dist/host.d.cts +4 -4
  51. package/dist/host.d.ts +4 -4
  52. package/dist/host.mjs +3 -3
  53. package/dist/index.cjs +34 -5
  54. package/dist/index.cjs.map +1 -1
  55. package/dist/index.d.cts +627 -69
  56. package/dist/index.d.ts +627 -69
  57. package/dist/index.mjs +33 -4
  58. package/dist/manifest.cjs +23 -132
  59. package/dist/manifest.cjs.map +1 -1
  60. package/dist/manifest.d.cts +12 -93
  61. package/dist/manifest.d.ts +12 -93
  62. package/dist/manifest.mjs +9 -118
  63. package/dist/manifest.mjs.map +1 -1
  64. package/dist/server.cjs +66 -14
  65. package/dist/server.cjs.map +1 -1
  66. package/dist/server.d.cts +6 -6
  67. package/dist/server.d.ts +6 -6
  68. package/dist/server.mjs +61 -9
  69. package/dist/server.mjs.map +1 -1
  70. package/dist/styles.css +367 -0
  71. package/dist/testing/index.cjs +13 -72
  72. package/dist/testing/index.cjs.map +1 -1
  73. package/dist/testing/index.d.cts +10 -29
  74. package/dist/testing/index.d.ts +10 -29
  75. package/dist/testing/index.mjs +13 -72
  76. package/dist/testing/index.mjs.map +1 -1
  77. package/dist/testing/jest.cjs +5 -5
  78. package/dist/testing/jest.mjs +2 -2
  79. package/dist/testing/vitest.cjs +5 -5
  80. package/dist/testing/vitest.cjs.map +1 -1
  81. package/dist/testing/vitest.d.cts +0 -10
  82. package/dist/testing/vitest.d.ts +0 -10
  83. package/dist/testing/vitest.mjs +2 -2
  84. package/dist/testing/vitest.mjs.map +1 -1
  85. package/dist/{types-BYGcp5pZ.d.ts → types-DDAGLpov.d.ts} +3 -4
  86. package/dist/{types-I1JOZyI_.d.cts → types-DrpQSv_2.d.cts} +3 -4
  87. package/dist/vite-preview.cjs +125 -294
  88. package/dist/vite-preview.cjs.map +1 -1
  89. package/dist/vite-preview.d.cts +24 -6
  90. package/dist/vite-preview.d.ts +24 -6
  91. package/dist/vite-preview.mjs +118 -287
  92. package/dist/vite-preview.mjs.map +1 -1
  93. package/dist/vite.cjs +23 -133
  94. package/dist/vite.cjs.map +1 -1
  95. package/dist/vite.d.cts +7 -40
  96. package/dist/vite.d.ts +7 -40
  97. package/dist/vite.mjs +17 -127
  98. package/dist/vite.mjs.map +1 -1
  99. package/package.json +2 -12
  100. package/schema/extension-v2.json +121 -432
  101. package/dist/chunk-2CY5HVY7.mjs +0 -59
  102. package/dist/chunk-2CY5HVY7.mjs.map +0 -1
  103. package/dist/chunk-7YKH4LZN.cjs.map +0 -1
  104. package/dist/chunk-A74HCO6T.cjs.map +0 -1
  105. package/dist/chunk-BPCGXXMD.cjs.map +0 -1
  106. package/dist/chunk-EB5XWQAU.cjs +0 -132
  107. package/dist/chunk-EB5XWQAU.cjs.map +0 -1
  108. package/dist/chunk-GAUZYBXE.cjs.map +0 -1
  109. package/dist/chunk-KZ5NGFUP.mjs.map +0 -1
  110. package/dist/chunk-O357OJXL.cjs.map +0 -1
  111. package/dist/chunk-ONJMPO3P.cjs +0 -59
  112. package/dist/chunk-ONJMPO3P.cjs.map +0 -1
  113. package/dist/chunk-QJMZRV7A.mjs +0 -132
  114. package/dist/chunk-QJMZRV7A.mjs.map +0 -1
  115. package/dist/chunk-QXD7WVKK.mjs.map +0 -1
  116. package/dist/chunk-SGCYEEXD.mjs.map +0 -1
  117. package/dist/chunk-SGEYDV3K.mjs.map +0 -1
  118. package/dist/chunk-SLGOHIBL.mjs.map +0 -1
  119. package/dist/chunk-SWG7CNTZ.mjs.map +0 -1
  120. package/dist/client-CV1qzQgx.d.cts +0 -25
  121. package/dist/client-CnlSHDWE.d.ts +0 -25
  122. package/dist/ui/styles.css +0 -740
  123. package/dist/ui.cjs +0 -1018
  124. package/dist/ui.cjs.map +0 -1
  125. package/dist/ui.d.cts +0 -231
  126. package/dist/ui.d.ts +0 -231
  127. package/dist/ui.mjs +0 -1018
  128. package/dist/ui.mjs.map +0 -1
  129. package/dist/vite-preview-bootstrap.cjs +0 -66
  130. package/dist/vite-preview-bootstrap.cjs.map +0 -1
  131. package/dist/vite-preview-bootstrap.d.cts +0 -15
  132. package/dist/vite-preview-bootstrap.d.ts +0 -15
  133. package/dist/vite-preview-bootstrap.mjs +0 -66
  134. package/dist/vite-preview-bootstrap.mjs.map +0 -1
  135. package/dist/work-events-DB3WR468.d.ts +0 -547
  136. package/dist/work-events-Dxe7Djdy.d.cts +0 -547
  137. /package/dist/{chunk-RBIQT75L.mjs.map → chunk-GG3IX52W.mjs.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/testing/index.cjs","../../src/testing/render.tsx","../../src/testing/validators.ts","../../src/testing/work-events.ts"],"names":[],"mappings":"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","file":"/home/runner/work/extension-sdk/extension-sdk/dist/testing/index.cjs","sourcesContent":[null,"import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport {\n render,\n type RenderOptions,\n type RenderResult,\n} from '@testing-library/react';\nimport type { ReactElement, ReactNode } from 'react';\nimport { installPrefix, procedurePrefix } from '../client/query';\nimport type { Tap } from '../client/types';\nimport { TapHostProvider } from '../host/provider';\nimport { createTapMock, type CreateTapMockOptions, type TapMock } from './mock';\n\n/**\n * What `renderWithTap` takes: its own three options, plus every\n * `CreateTapMockOptions` member it forwards to the mock.\n */\nexport interface RenderWithTapOptions extends CreateTapMockOptions {\n /** A mock you built yourself. Given, every mock option here is ignored. */\n mock?: TapMock;\n /** Your own client, for a test that reads the cache. Defaults to a fresh one. */\n queryClient?: QueryClient;\n /** Passed to Testing Library's `render`, for a mount point of your own. */\n container?: RenderOptions['container'];\n}\n\n/** What `renderWithTap` returns: Testing Library's result plus the bridge. */\nexport interface RenderWithTapResult extends RenderResult {\n /** The same object `useTap` returns inside the component. */\n tap: Tap;\n /** The mock behind the bridge, which the matchers take. */\n mock: TapMock;\n /** The client the render used, whether it was passed in or built here. */\n queryClient: QueryClient;\n}\n\n/**\n * Renders a component against the mock bridge, so a widget, tab, page, row action\n * or settings panel is testable outside Tappify.\n *\n * @remarks\n * It builds a mock from the same options `createTapMock` takes unless you pass\n * `mock`, wraps the component in the host provider and a `QueryClient` that does\n * not retry and does not cache between tests, and appends the mock's portal to the\n * rendered document so SDK dialogs have somewhere to go. It rewires\n * `tap.invalidate` onto that client, so a component calling it drops the right\n * cache entries. Scopes default to all of them, so pass `scopes` to cover the\n * refused path; a `handler` has to be built with `jwks: await testJwks()` and the\n * same `extensionId` the mock reports. Load\n * `@tappify/extension-sdk/testing/vitest` or `.../testing/jest` as a setup file\n * first: that installs the host CSS variables, registers the matchers and clears\n * the portal between tests.\n *\n * @example\n * ```tsx\n * import { useTapQuery } from '@tappify/extension-sdk';\n * import { Metric } from '@tappify/extension-sdk/ui';\n * import { renderWithTap } from '@tappify/extension-sdk/testing';\n * import { screen, waitFor } from '@testing-library/react';\n * import { expect, it } from 'vitest';\n *\n * function Crashes() {\n * const crashes = useTapQuery({\n * kind: 'crashes',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * return <Metric label=\"Issues\" value={crashes.data?.issues.length ?? 0} />;\n * }\n *\n * it('renders the issues the fixtures answer with', async () => {\n * renderWithTap(<Crashes />, { scopes: ['ui:render', 'crashes:read'] });\n * await waitFor(() => expect(screen.getByText('2')).toBeTruthy());\n * });\n * ```\n */\nexport function renderWithTap(\n ui: ReactElement,\n options: RenderWithTapOptions = {},\n): RenderWithTapResult {\n const mock = options.mock ?? createTapMock(options);\n const queryClient =\n options.queryClient ??\n new QueryClient({\n defaultOptions: { queries: { retry: false, gcTime: 0 } },\n });\n\n const installId = mock.tap.extension.installId;\n mock.tap.invalidate = name => {\n void queryClient.invalidateQueries({\n queryKey:\n name === undefined\n ? installPrefix(installId)\n : procedurePrefix(installId, name),\n });\n };\n\n function Wrapper({ children }: { children: ReactNode }): ReactElement {\n return (\n <QueryClientProvider client={queryClient}>\n <TapHostProvider tap={mock.tap}>{children}</TapHostProvider>\n </QueryClientProvider>\n );\n }\n\n const result = render(ui, { wrapper: Wrapper, container: options.container });\n result.baseElement.append(mock.portal);\n\n return { ...result, tap: mock.tap, mock, queryClient };\n}\n","import { CARD_KEYS } from '../manifest/constants';\nimport type { MetricDeclaration, ToolDeclaration } from '../manifest/types';\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return typeof value === 'object' && value !== null\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction list(value: unknown): unknown[] | null {\n return Array.isArray(value) ? value : null;\n}\n\nfunction isPoint(value: unknown): boolean {\n const pair = list(value);\n return (\n pair !== null &&\n pair.length === 2 &&\n typeof pair[0] === 'string' &&\n typeof pair[1] === 'number'\n );\n}\n\n/**\n * Checks a metrics response against the metrics the manifest declares and returns\n * what is wrong with it.\n *\n * @remarks\n * Returns an empty array when the response is sound, so a test asserts on an empty\n * list. It reports a body that is not `{ series: [...] }`, an entry with no\n * `metric` key, a metric the declarations do not name, a unit that disagrees with\n * the declared one, a missing `points` array, and any point that is not a\n * `[string, number]` pair. It does not check that every declared metric is\n * answered, so assert on the length yourself when that matters. The strings are the\n * ones `tappify extension doctor` reports.\n *\n * @example\n * ```ts\n * import { validateMetricsResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * const metrics = [\n * { key: 'orders', label: 'Orders', unit: 'count' as const, kind: 'counter' as const },\n * ];\n *\n * it('answers orders as a counted series', () => {\n * const response = { series: [{ metric: 'orders', unit: 'count', points: [['2026-09-01', 12]] }] };\n * expect(validateMetricsResponse(response, metrics)).toEqual([]);\n * });\n * ```\n */\nexport function validateMetricsResponse(\n response: unknown,\n metrics: MetricDeclaration[],\n): string[] {\n const series = list(record(response)?.series);\n\n if (series === null) {\n return [\n 'A metrics response is { \"series\": [...] }. Return one entry per declared metric, even when it has no points.',\n ];\n }\n\n const issues: string[] = [];\n const declared = new Map(metrics.map(metric => [metric.key, metric]));\n\n for (const [index, entry] of series.entries()) {\n const item = record(entry);\n const key = typeof item?.metric === 'string' ? item.metric : null;\n\n if (item === null || key === null) {\n issues.push(`series[${String(index)}] has no \"metric\" key.`);\n continue;\n }\n\n const metric = declared.get(key);\n if (metric === undefined) {\n issues.push(\n `series[${String(index)}] reports \"${key}\", which is not declared under contributes.connector.metrics.`,\n );\n continue;\n }\n\n if (item.unit !== metric.unit) {\n issues.push(\n `series[${String(index)}] reports unit \"${String(item.unit)}\" but \"${key}\" is declared as \"${metric.unit}\".`,\n );\n }\n\n const points = list(item.points);\n if (points === null) {\n issues.push(`series[${String(index)}] has no \"points\" array.`);\n continue;\n }\n\n for (const [pointIndex, point] of points.entries()) {\n if (!isPoint(point)) {\n issues.push(\n `series[${String(index)}].points[${String(pointIndex)}] must be a two-element [timestamp, value] pair: a string and a number.`,\n );\n }\n }\n }\n\n return issues;\n}\n\n/**\n * Checks a tool's answer against the card shape its declared `returns` kind has to\n * carry, and returns what is missing.\n *\n * @remarks\n * Returns an empty array when the answer is sound. A `value` card needs `label` and\n * `value`, a `series` card `label` and `points`, a `table` card `columns` and\n * `rows`, a `list` card `items`, and a `comparison` card `label`, `left` and\n * `right`; anything that is not an object is reported on its own. It checks the\n * keys are present, not what they hold, so an empty `rows` array passes.\n *\n * @example\n * ```ts\n * import { validateToolResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * it('answers keyword_gaps with a table card', () => {\n * const card = { columns: [{ key: 'term', label: 'Term' }], rows: [] };\n * expect(validateToolResponse(card, 'table')).toEqual([]);\n * });\n * ```\n */\nexport function validateToolResponse(\n value: unknown,\n returns: ToolDeclaration['returns'],\n): string[] {\n const body = record(value);\n if (body === null) {\n return [\n `A \"${returns}\" tool returns an object the host draws as a card, not ${typeof value}.`,\n ];\n }\n\n return CARD_KEYS[returns]\n .filter(key => !(key in body))\n .map(\n key =>\n `A \"${returns}\" tool response needs a \"${key}\" key; the host draws the card from it.`,\n );\n}\n","import type {\n WorkHydratedCommentCreatedEvent,\n WorkHydratedItemDeletedEvent,\n WorkHydratedStatusChangedEvent,\n} from '../server/work';\nimport {\n sendWorkEvent,\n type SendWorkEventOptions,\n type WorkEvent,\n} from '../server/work-events';\n\n/** A fresh complete comment signal with deterministic identity and shallow overrides. */\nexport function workCommentCreatedFixture(\n overrides: Partial<WorkHydratedCommentCreatedEvent> = {},\n): WorkHydratedCommentCreatedEvent {\n return {\n type: 'comment.created',\n externalItemId: 'item_test',\n providerEventId: 'event_comment_test',\n occurredAt: '2026-10-15T12:00:00.000Z',\n dedupeKey: 'item_test:event_comment_test',\n comment: {\n externalCommentId: 'comment_test',\n body: 'A provider comment.',\n author: { id: 'author_test', displayName: 'Sam' },\n createdAt: '2026-10-15T12:00:00.000Z',\n },\n ...overrides,\n };\n}\n\n/** A fresh complete status signal with deterministic identity and shallow overrides. */\nexport function workStatusChangedFixture(\n overrides: Partial<WorkHydratedStatusChangedEvent> = {},\n): WorkHydratedStatusChangedEvent {\n return {\n type: 'status.changed',\n externalItemId: 'item_test',\n providerEventId: 'event_status_test',\n occurredAt: '2026-10-15T12:00:00.000Z',\n dedupeKey: 'item_test:event_status_test',\n status: { id: 'resolved_test', label: 'Resolved', stage: 'done' },\n ...overrides,\n };\n}\n\n/** A fresh deletion signal with deterministic identity and shallow overrides. */\nexport function workItemDeletedFixture(\n overrides: Partial<WorkHydratedItemDeletedEvent> = {},\n): WorkHydratedItemDeletedEvent {\n return {\n type: 'item.deleted',\n externalItemId: 'item_test',\n providerEventId: 'event_deleted_test',\n occurredAt: '2026-10-15T12:00:00.000Z',\n dedupeKey: 'item_test:event_deleted_test',\n ...overrides,\n };\n}\n\n/**\n * Captures a signed work-event Request through the real sender without network I/O.\n *\n * @remarks\n * Uses the same validation, serialization, signing and callback URL as\n * sendWorkEvent. Supply the callback and secret your test install was configured\n * with; the accepting transport is supplied by this helper.\n */\nexport async function signedWorkEventRequest(\n event: WorkEvent,\n options: Omit<SendWorkEventOptions, 'fetch'>,\n): Promise<Request> {\n let request: Request | undefined;\n await sendWorkEvent(event, {\n ...options,\n fetch: (input, init) => {\n request = new Request(input, init);\n return Promise.resolve(new Response(null, { status: 202 }));\n },\n });\n if (!request)\n throw new Error('The work event sender did not produce a request.');\n return request;\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/testing/index.cjs","../../src/testing/render.tsx","../../src/testing/validators.ts"],"names":[],"mappings":"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","file":"/home/runner/work/extension-sdk/extension-sdk/dist/testing/index.cjs","sourcesContent":[null,"import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport {\n render,\n type RenderOptions,\n type RenderResult,\n} from '@testing-library/react';\nimport type { ReactElement, ReactNode } from 'react';\nimport { installPrefix, procedurePrefix } from '../client/query';\nimport type { Tap } from '../client/types';\nimport { TapHostProvider } from '../host/provider';\nimport { createTapMock, type CreateTapMockOptions, type TapMock } from './mock';\n\n/**\n * What `renderWithTap` takes: its own three options, plus every\n * `CreateTapMockOptions` member it forwards to the mock.\n */\nexport interface RenderWithTapOptions extends CreateTapMockOptions {\n /** A mock you built yourself. Given, every mock option here is ignored. */\n mock?: TapMock;\n /** Your own client, for a test that reads the cache. Defaults to a fresh one. */\n queryClient?: QueryClient;\n /** Passed to Testing Library's `render`, for a mount point of your own. */\n container?: RenderOptions['container'];\n}\n\n/** What `renderWithTap` returns: Testing Library's result plus the bridge. */\nexport interface RenderWithTapResult extends RenderResult {\n /** The same object `useTap` returns inside the component. */\n tap: Tap;\n /** The mock behind the bridge, which the matchers take. */\n mock: TapMock;\n /** The client the render used, whether it was passed in or built here. */\n queryClient: QueryClient;\n}\n\n/**\n * Renders a component against the mock bridge, so a widget, tab, page, row action\n * or settings panel is testable outside Tappify.\n *\n * @remarks\n * It builds a mock from the same options `createTapMock` takes unless you pass\n * `mock`, wraps the component in the host provider and a `QueryClient` that does\n * not retry and does not cache between tests, and appends the mock's portal to the\n * rendered document so `TapDialog` has somewhere to go. It rewires\n * `tap.invalidate` onto that client, so a component calling it drops the right\n * cache entries. Scopes default to all of them, so pass `scopes` to cover the\n * refused path; a `handler` has to be built with `jwks: await testJwks()` and the\n * same `extensionId` the mock reports. Load\n * `@tappify/extension-sdk/testing/vitest` or `.../testing/jest` as a setup file\n * first: that installs the host CSS variables, registers the matchers and clears\n * the portal between tests.\n *\n * @example\n * ```tsx\n * import { TapStat, useTapQuery } from '@tappify/extension-sdk';\n * import { renderWithTap } from '@tappify/extension-sdk/testing';\n * import { screen, waitFor } from '@testing-library/react';\n * import { expect, it } from 'vitest';\n *\n * function Crashes() {\n * const crashes = useTapQuery({\n * kind: 'crashes',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * return <TapStat label=\"Issues\" value={crashes.data?.issues.length ?? 0} />;\n * }\n *\n * it('renders the issues the fixtures answer with', async () => {\n * renderWithTap(<Crashes />, { scopes: ['ui:render', 'crashes:read'] });\n * await waitFor(() => expect(screen.getByText('2')).toBeTruthy());\n * });\n * ```\n */\nexport function renderWithTap(\n ui: ReactElement,\n options: RenderWithTapOptions = {},\n): RenderWithTapResult {\n const mock = options.mock ?? createTapMock(options);\n const queryClient =\n options.queryClient ??\n new QueryClient({\n defaultOptions: { queries: { retry: false, gcTime: 0 } },\n });\n\n const installId = mock.tap.extension.installId;\n mock.tap.invalidate = name => {\n void queryClient.invalidateQueries({\n queryKey:\n name === undefined\n ? installPrefix(installId)\n : procedurePrefix(installId, name),\n });\n };\n\n function Wrapper({ children }: { children: ReactNode }): ReactElement {\n return (\n <QueryClientProvider client={queryClient}>\n <TapHostProvider tap={mock.tap}>{children}</TapHostProvider>\n </QueryClientProvider>\n );\n }\n\n const result = render(ui, { wrapper: Wrapper, container: options.container });\n result.baseElement.append(mock.portal);\n\n return { ...result, tap: mock.tap, mock, queryClient };\n}\n","import { CARD_KEYS } from '../manifest/constants';\nimport type { MetricDeclaration, ToolDeclaration } from '../manifest/types';\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return typeof value === 'object' && value !== null\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction list(value: unknown): unknown[] | null {\n return Array.isArray(value) ? value : null;\n}\n\nfunction isPoint(value: unknown): boolean {\n const pair = list(value);\n return (\n pair !== null &&\n pair.length === 2 &&\n typeof pair[0] === 'string' &&\n typeof pair[1] === 'number'\n );\n}\n\n/**\n * Checks a metrics response against the metrics the manifest declares and returns\n * what is wrong with it.\n *\n * @remarks\n * Returns an empty array when the response is sound, so a test asserts on an empty\n * list. It reports a body that is not `{ series: [...] }`, an entry with no\n * `metric` key, a metric the declarations do not name, a unit that disagrees with\n * the declared one, a missing `points` array, and any point that is not a\n * `[string, number]` pair. It does not check that every declared metric is\n * answered, so assert on the length yourself when that matters. The strings are the\n * ones `tappify extension doctor` reports.\n *\n * @example\n * ```ts\n * import { validateMetricsResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * const metrics = [\n * { key: 'orders', label: 'Orders', unit: 'count' as const, kind: 'counter' as const },\n * ];\n *\n * it('answers orders as a counted series', () => {\n * const response = { series: [{ metric: 'orders', unit: 'count', points: [['2026-09-01', 12]] }] };\n * expect(validateMetricsResponse(response, metrics)).toEqual([]);\n * });\n * ```\n */\nexport function validateMetricsResponse(\n response: unknown,\n metrics: MetricDeclaration[],\n): string[] {\n const series = list(record(response)?.series);\n\n if (series === null) {\n return [\n 'A metrics response is { \"series\": [...] }. Return one entry per declared metric, even when it has no points.',\n ];\n }\n\n const issues: string[] = [];\n const declared = new Map(metrics.map(metric => [metric.key, metric]));\n\n for (const [index, entry] of series.entries()) {\n const item = record(entry);\n const key = typeof item?.metric === 'string' ? item.metric : null;\n\n if (item === null || key === null) {\n issues.push(`series[${String(index)}] has no \"metric\" key.`);\n continue;\n }\n\n const metric = declared.get(key);\n if (metric === undefined) {\n issues.push(\n `series[${String(index)}] reports \"${key}\", which is not declared under contributes.connector.metrics.`,\n );\n continue;\n }\n\n if (item.unit !== metric.unit) {\n issues.push(\n `series[${String(index)}] reports unit \"${String(item.unit)}\" but \"${key}\" is declared as \"${metric.unit}\".`,\n );\n }\n\n const points = list(item.points);\n if (points === null) {\n issues.push(`series[${String(index)}] has no \"points\" array.`);\n continue;\n }\n\n for (const [pointIndex, point] of points.entries()) {\n if (!isPoint(point)) {\n issues.push(\n `series[${String(index)}].points[${String(pointIndex)}] must be a two-element [timestamp, value] pair: a string and a number.`,\n );\n }\n }\n }\n\n return issues;\n}\n\n/**\n * Checks a tool's answer against the card shape its declared `returns` kind has to\n * carry, and returns what is missing.\n *\n * @remarks\n * Returns an empty array when the answer is sound. A `value` card needs `label` and\n * `value`, a `series` card `label` and `points`, a `table` card `columns` and\n * `rows`, a `list` card `items`, and a `comparison` card `label`, `left` and\n * `right`; anything that is not an object is reported on its own. It checks the\n * keys are present, not what they hold, so an empty `rows` array passes.\n *\n * @example\n * ```ts\n * import { validateToolResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * it('answers keyword_gaps with a table card', () => {\n * const card = { columns: [{ key: 'term', label: 'Term' }], rows: [] };\n * expect(validateToolResponse(card, 'table')).toEqual([]);\n * });\n * ```\n */\nexport function validateToolResponse(\n value: unknown,\n returns: ToolDeclaration['returns'],\n): string[] {\n const body = record(value);\n if (body === null) {\n return [\n `A \"${returns}\" tool returns an object the host draws as a card, not ${typeof value}.`,\n ];\n }\n\n return CARD_KEYS[returns]\n .filter(key => !(key in body))\n .map(\n key =>\n `A \"${returns}\" tool response needs a \"${key}\" key; the host draws the card from it.`,\n );\n}\n"]}
@@ -1,8 +1,8 @@
1
1
  import { JSONWebKeySet } from 'jose';
2
- import { a as TapFilters, N as TapProjectResult, o as TapApp, Q as TapRelease, D as TapKeyword, F as TapListing, U as TapReviewsResult, v as TapCrashesResult, R as TapRevenueResult, W as TapSeriesMetric, Y as TapSeriesResult, d as TapQuery, e as TapResult, b as TapSize, T as Tap, a6 as TapToastTone, t as TapConfirmOptions, q as TapChatCard, c as TapHostEvents } from '../types-I1JOZyI_.cjs';
3
- import { k as TappifyHealth, b as MetricsInput, c as MetricsResponse, M as MentionItems, C as ContextBlock, Z as WorkOperationMap, l as TappifyInstall, j as TappifyHandlerOptions, a as TappifyClaims, T as TappifyFetchHandler, B as WorkEvent, S as SendWorkEventOptions, F as WorkHydratedCommentCreatedEvent, I as WorkHydratedItemDeletedEvent, J as WorkHydratedStatusChangedEvent } from '../work-events-Dxe7Djdy.cjs';
2
+ import { a as TapFilters, N as TapProjectResult, o as TapApp, Q as TapRelease, D as TapKeyword, F as TapListing, U as TapReviewsResult, v as TapCrashesResult, R as TapRevenueResult, W as TapSeriesMetric, Y as TapSeriesResult, d as TapQuery, e as TapResult, b as TapSize, T as Tap, a6 as TapToastTone, t as TapConfirmOptions, q as TapChatCard, c as TapHostEvents } from '../types-DrpQSv_2.cjs';
3
+ import { h as TappifyHealth, b as MetricsInput, c as MetricsResponse, M as MentionItems, C as ContextBlock, W as WorkOperation, i as TappifyInstall, g as TappifyHandlerOptions, a as TappifyClaims, T as TappifyFetchHandler } from '../handler-kU9Jf36K.cjs';
4
4
  export { M as MatcherResult, a as MatcherTarget, r as registerTapMatchers, t as tapMatchers } from '../matchers-Cz93AK1n.cjs';
5
- import { S as ScopeKey, x as MetricDeclaration, a4 as ToolDeclaration } from '../constants-BAimnqOK.cjs';
5
+ import { S as ScopeKey, x as MetricDeclaration, a3 as ToolDeclaration } from '../constants-DJ_KgZpN.cjs';
6
6
  import { QueryClient } from '@tanstack/react-query';
7
7
  import { RenderOptions, RenderResult } from '@testing-library/react';
8
8
  import { ReactElement } from 'react';
@@ -91,9 +91,7 @@ interface TestClient {
91
91
  /** Posts one Tappify event, which the handler answers with 204. */
92
92
  event(name: string, payload: Record<string, unknown>): Promise<void>;
93
93
  /** Calls one work operation. */
94
- work<O extends keyof WorkOperationMap>(operation: O, input: WorkOperationMap[O]['input']): Promise<WorkOperationMap[O]['output']>;
95
- /** @deprecated Call set_status or describe_container using the current work protocol. */
96
- work(operation: 'status' | 'resolve', input: unknown): Promise<unknown>;
94
+ work(operation: WorkOperation, input: unknown): Promise<unknown>;
97
95
  /**
98
96
  * Sends any method and path and returns the `Response`, for a test that asserts
99
97
  * on a status rather than a body.
@@ -140,7 +138,7 @@ interface TestClient {
140
138
  declare function createTestClient(target: TappifyHandlerOptions, defaults?: TestClientDefaults): TestClient;
141
139
 
142
140
  /**
143
- * Sets the host's eleven One UI CSS variables on an element, so a component renders in
141
+ * Sets the host's fifteen CSS variables on an element, so a component renders in
144
142
  * a test the way it renders in the host.
145
143
  *
146
144
  * @remarks
@@ -350,7 +348,7 @@ interface TapMock {
350
348
  /** The bridge itself, which `renderWithTap` puts above the component. */
351
349
  tap: Tap;
352
350
  calls: TapMockCalls;
353
- /** The node SDK dialogs render into, appended to the document by `renderWithTap`. */
351
+ /** The node `TapDialog` renders into, appended to the document by `renderWithTap`. */
354
352
  portal: HTMLElement;
355
353
  /** What a collection holds now, or just the one document under `documentId`. */
356
354
  documents(collection: string, documentId?: string): unknown[];
@@ -426,7 +424,7 @@ interface RenderWithTapResult extends RenderResult {
426
424
  * It builds a mock from the same options `createTapMock` takes unless you pass
427
425
  * `mock`, wraps the component in the host provider and a `QueryClient` that does
428
426
  * not retry and does not cache between tests, and appends the mock's portal to the
429
- * rendered document so SDK dialogs have somewhere to go. It rewires
427
+ * rendered document so `TapDialog` has somewhere to go. It rewires
430
428
  * `tap.invalidate` onto that client, so a component calling it drops the right
431
429
  * cache entries. Scopes default to all of them, so pass `scopes` to cover the
432
430
  * refused path; a `handler` has to be built with `jwks: await testJwks()` and the
@@ -437,8 +435,7 @@ interface RenderWithTapResult extends RenderResult {
437
435
  *
438
436
  * @example
439
437
  * ```tsx
440
- * import { useTapQuery } from '@tappify/extension-sdk';
441
- * import { Metric } from '@tappify/extension-sdk/ui';
438
+ * import { TapStat, useTapQuery } from '@tappify/extension-sdk';
442
439
  * import { renderWithTap } from '@tappify/extension-sdk/testing';
443
440
  * import { screen, waitFor } from '@testing-library/react';
444
441
  * import { expect, it } from 'vitest';
@@ -448,7 +445,7 @@ interface RenderWithTapResult extends RenderResult {
448
445
  * kind: 'crashes',
449
446
  * range: { from: '2026-09-01', to: '2026-09-08' },
450
447
  * });
451
- * return <Metric label="Issues" value={crashes.data?.issues.length ?? 0} />;
448
+ * return <TapStat label="Issues" value={crashes.data?.issues.length ?? 0} />;
452
449
  * }
453
450
  *
454
451
  * it('renders the issues the fixtures answer with', async () => {
@@ -512,20 +509,4 @@ declare function validateMetricsResponse(response: unknown, metrics: MetricDecla
512
509
  */
513
510
  declare function validateToolResponse(value: unknown, returns: ToolDeclaration['returns']): string[];
514
511
 
515
- /** A fresh complete comment signal with deterministic identity and shallow overrides. */
516
- declare function workCommentCreatedFixture(overrides?: Partial<WorkHydratedCommentCreatedEvent>): WorkHydratedCommentCreatedEvent;
517
- /** A fresh complete status signal with deterministic identity and shallow overrides. */
518
- declare function workStatusChangedFixture(overrides?: Partial<WorkHydratedStatusChangedEvent>): WorkHydratedStatusChangedEvent;
519
- /** A fresh deletion signal with deterministic identity and shallow overrides. */
520
- declare function workItemDeletedFixture(overrides?: Partial<WorkHydratedItemDeletedEvent>): WorkHydratedItemDeletedEvent;
521
- /**
522
- * Captures a signed work-event Request through the real sender without network I/O.
523
- *
524
- * @remarks
525
- * Uses the same validation, serialization, signing and callback URL as
526
- * sendWorkEvent. Supply the callback and secret your test install was configured
527
- * with; the accepting transport is supplied by this helper.
528
- */
529
- declare function signedWorkEventRequest(event: WorkEvent, options: Omit<SendWorkEventOptions, 'fetch'>): Promise<Request>;
530
-
531
- export { type CreateTapMockOptions, type RenderWithTapOptions, type RenderWithTapResult, type TapFixtures, type TapMock, type TapMockCalls, type TestClient, type TestClientDefaults, answerFixtureQuery, createTapMock, createTestClient, fixtureApps, fixtureCrashes, fixtureKeywords, fixtureListings, fixtureProject, fixtureReleases, fixtureRevenue, fixtureReviews, fixtureSeries, fixtures, installHostTheme, renderWithTap, signTestToken, signedWorkEventRequest, testJwks, validateMetricsResponse, validateToolResponse, workCommentCreatedFixture, workItemDeletedFixture, workStatusChangedFixture };
512
+ export { type CreateTapMockOptions, type RenderWithTapOptions, type RenderWithTapResult, type TapFixtures, type TapMock, type TapMockCalls, type TestClient, type TestClientDefaults, answerFixtureQuery, createTapMock, createTestClient, fixtureApps, fixtureCrashes, fixtureKeywords, fixtureListings, fixtureProject, fixtureReleases, fixtureRevenue, fixtureReviews, fixtureSeries, fixtures, installHostTheme, renderWithTap, signTestToken, testJwks, validateMetricsResponse, validateToolResponse };
@@ -1,8 +1,8 @@
1
1
  import { JSONWebKeySet } from 'jose';
2
- import { a as TapFilters, N as TapProjectResult, o as TapApp, Q as TapRelease, D as TapKeyword, F as TapListing, U as TapReviewsResult, v as TapCrashesResult, R as TapRevenueResult, W as TapSeriesMetric, Y as TapSeriesResult, d as TapQuery, e as TapResult, b as TapSize, T as Tap, a6 as TapToastTone, t as TapConfirmOptions, q as TapChatCard, c as TapHostEvents } from '../types-BYGcp5pZ.js';
3
- import { k as TappifyHealth, b as MetricsInput, c as MetricsResponse, M as MentionItems, C as ContextBlock, Z as WorkOperationMap, l as TappifyInstall, j as TappifyHandlerOptions, a as TappifyClaims, T as TappifyFetchHandler, B as WorkEvent, S as SendWorkEventOptions, F as WorkHydratedCommentCreatedEvent, I as WorkHydratedItemDeletedEvent, J as WorkHydratedStatusChangedEvent } from '../work-events-DB3WR468.js';
2
+ import { a as TapFilters, N as TapProjectResult, o as TapApp, Q as TapRelease, D as TapKeyword, F as TapListing, U as TapReviewsResult, v as TapCrashesResult, R as TapRevenueResult, W as TapSeriesMetric, Y as TapSeriesResult, d as TapQuery, e as TapResult, b as TapSize, T as Tap, a6 as TapToastTone, t as TapConfirmOptions, q as TapChatCard, c as TapHostEvents } from '../types-DDAGLpov.js';
3
+ import { h as TappifyHealth, b as MetricsInput, c as MetricsResponse, M as MentionItems, C as ContextBlock, W as WorkOperation, i as TappifyInstall, g as TappifyHandlerOptions, a as TappifyClaims, T as TappifyFetchHandler } from '../handler-CZIlZ5vu.js';
4
4
  export { M as MatcherResult, a as MatcherTarget, r as registerTapMatchers, t as tapMatchers } from '../matchers-Cz93AK1n.js';
5
- import { S as ScopeKey, x as MetricDeclaration, a4 as ToolDeclaration } from '../constants-BAimnqOK.js';
5
+ import { S as ScopeKey, x as MetricDeclaration, a3 as ToolDeclaration } from '../constants-DJ_KgZpN.js';
6
6
  import { QueryClient } from '@tanstack/react-query';
7
7
  import { RenderOptions, RenderResult } from '@testing-library/react';
8
8
  import { ReactElement } from 'react';
@@ -91,9 +91,7 @@ interface TestClient {
91
91
  /** Posts one Tappify event, which the handler answers with 204. */
92
92
  event(name: string, payload: Record<string, unknown>): Promise<void>;
93
93
  /** Calls one work operation. */
94
- work<O extends keyof WorkOperationMap>(operation: O, input: WorkOperationMap[O]['input']): Promise<WorkOperationMap[O]['output']>;
95
- /** @deprecated Call set_status or describe_container using the current work protocol. */
96
- work(operation: 'status' | 'resolve', input: unknown): Promise<unknown>;
94
+ work(operation: WorkOperation, input: unknown): Promise<unknown>;
97
95
  /**
98
96
  * Sends any method and path and returns the `Response`, for a test that asserts
99
97
  * on a status rather than a body.
@@ -140,7 +138,7 @@ interface TestClient {
140
138
  declare function createTestClient(target: TappifyHandlerOptions, defaults?: TestClientDefaults): TestClient;
141
139
 
142
140
  /**
143
- * Sets the host's eleven One UI CSS variables on an element, so a component renders in
141
+ * Sets the host's fifteen CSS variables on an element, so a component renders in
144
142
  * a test the way it renders in the host.
145
143
  *
146
144
  * @remarks
@@ -350,7 +348,7 @@ interface TapMock {
350
348
  /** The bridge itself, which `renderWithTap` puts above the component. */
351
349
  tap: Tap;
352
350
  calls: TapMockCalls;
353
- /** The node SDK dialogs render into, appended to the document by `renderWithTap`. */
351
+ /** The node `TapDialog` renders into, appended to the document by `renderWithTap`. */
354
352
  portal: HTMLElement;
355
353
  /** What a collection holds now, or just the one document under `documentId`. */
356
354
  documents(collection: string, documentId?: string): unknown[];
@@ -426,7 +424,7 @@ interface RenderWithTapResult extends RenderResult {
426
424
  * It builds a mock from the same options `createTapMock` takes unless you pass
427
425
  * `mock`, wraps the component in the host provider and a `QueryClient` that does
428
426
  * not retry and does not cache between tests, and appends the mock's portal to the
429
- * rendered document so SDK dialogs have somewhere to go. It rewires
427
+ * rendered document so `TapDialog` has somewhere to go. It rewires
430
428
  * `tap.invalidate` onto that client, so a component calling it drops the right
431
429
  * cache entries. Scopes default to all of them, so pass `scopes` to cover the
432
430
  * refused path; a `handler` has to be built with `jwks: await testJwks()` and the
@@ -437,8 +435,7 @@ interface RenderWithTapResult extends RenderResult {
437
435
  *
438
436
  * @example
439
437
  * ```tsx
440
- * import { useTapQuery } from '@tappify/extension-sdk';
441
- * import { Metric } from '@tappify/extension-sdk/ui';
438
+ * import { TapStat, useTapQuery } from '@tappify/extension-sdk';
442
439
  * import { renderWithTap } from '@tappify/extension-sdk/testing';
443
440
  * import { screen, waitFor } from '@testing-library/react';
444
441
  * import { expect, it } from 'vitest';
@@ -448,7 +445,7 @@ interface RenderWithTapResult extends RenderResult {
448
445
  * kind: 'crashes',
449
446
  * range: { from: '2026-09-01', to: '2026-09-08' },
450
447
  * });
451
- * return <Metric label="Issues" value={crashes.data?.issues.length ?? 0} />;
448
+ * return <TapStat label="Issues" value={crashes.data?.issues.length ?? 0} />;
452
449
  * }
453
450
  *
454
451
  * it('renders the issues the fixtures answer with', async () => {
@@ -512,20 +509,4 @@ declare function validateMetricsResponse(response: unknown, metrics: MetricDecla
512
509
  */
513
510
  declare function validateToolResponse(value: unknown, returns: ToolDeclaration['returns']): string[];
514
511
 
515
- /** A fresh complete comment signal with deterministic identity and shallow overrides. */
516
- declare function workCommentCreatedFixture(overrides?: Partial<WorkHydratedCommentCreatedEvent>): WorkHydratedCommentCreatedEvent;
517
- /** A fresh complete status signal with deterministic identity and shallow overrides. */
518
- declare function workStatusChangedFixture(overrides?: Partial<WorkHydratedStatusChangedEvent>): WorkHydratedStatusChangedEvent;
519
- /** A fresh deletion signal with deterministic identity and shallow overrides. */
520
- declare function workItemDeletedFixture(overrides?: Partial<WorkHydratedItemDeletedEvent>): WorkHydratedItemDeletedEvent;
521
- /**
522
- * Captures a signed work-event Request through the real sender without network I/O.
523
- *
524
- * @remarks
525
- * Uses the same validation, serialization, signing and callback URL as
526
- * sendWorkEvent. Supply the callback and secret your test install was configured
527
- * with; the accepting transport is supplied by this helper.
528
- */
529
- declare function signedWorkEventRequest(event: WorkEvent, options: Omit<SendWorkEventOptions, 'fetch'>): Promise<Request>;
530
-
531
- export { type CreateTapMockOptions, type RenderWithTapOptions, type RenderWithTapResult, type TapFixtures, type TapMock, type TapMockCalls, type TestClient, type TestClientDefaults, answerFixtureQuery, createTapMock, createTestClient, fixtureApps, fixtureCrashes, fixtureKeywords, fixtureListings, fixtureProject, fixtureReleases, fixtureRevenue, fixtureReviews, fixtureSeries, fixtures, installHostTheme, renderWithTap, signTestToken, signedWorkEventRequest, testJwks, validateMetricsResponse, validateToolResponse, workCommentCreatedFixture, workItemDeletedFixture, workStatusChangedFixture };
512
+ export { type CreateTapMockOptions, type RenderWithTapOptions, type RenderWithTapResult, type TapFixtures, type TapMock, type TapMockCalls, type TestClient, type TestClientDefaults, answerFixtureQuery, createTapMock, createTestClient, fixtureApps, fixtureCrashes, fixtureKeywords, fixtureListings, fixtureProject, fixtureReleases, fixtureRevenue, fixtureReviews, fixtureSeries, fixtures, installHostTheme, renderWithTap, signTestToken, testJwks, validateMetricsResponse, validateToolResponse };
@@ -1,8 +1,3 @@
1
- import {
2
- installHostTheme,
3
- registerTapMatchers,
4
- tapMatchers
5
- } from "../chunk-SLGOHIBL.mjs";
6
1
  import {
7
2
  answerFixtureQuery,
8
3
  createTapMock,
@@ -19,23 +14,25 @@ import {
19
14
  fixtures,
20
15
  signTestToken,
21
16
  testJwks
22
- } from "../chunk-SWG7CNTZ.mjs";
17
+ } from "../chunk-CTSZ5D7S.mjs";
23
18
  import {
24
19
  installPrefix,
25
20
  procedurePrefix
26
- } from "../chunk-SGEYDV3K.mjs";
21
+ } from "../chunk-BDCX6LRQ.mjs";
27
22
  import {
28
23
  TapHostProvider
29
- } from "../chunk-RBIQT75L.mjs";
30
- import "../chunk-QA5AL3VZ.mjs";
31
- import {
32
- sendWorkEvent
33
- } from "../chunk-QJMZRV7A.mjs";
34
- import "../chunk-KZ5NGFUP.mjs";
24
+ } from "../chunk-GG3IX52W.mjs";
25
+ import "../chunk-QHM7XNRK.mjs";
26
+ import "../chunk-C3YNVHBP.mjs";
35
27
  import {
36
28
  CARD_KEYS
37
- } from "../chunk-SGCYEEXD.mjs";
38
- import "../chunk-QXD7WVKK.mjs";
29
+ } from "../chunk-MNDVYPEP.mjs";
30
+ import {
31
+ installHostTheme,
32
+ registerTapMatchers,
33
+ tapMatchers
34
+ } from "../chunk-IGK2AP6J.mjs";
35
+ import "../chunk-CSTYBKZC.mjs";
39
36
  import "../chunk-J5LGTIGS.mjs";
40
37
 
41
38
  // src/testing/render.tsx
@@ -128,58 +125,6 @@ function validateToolResponse(value, returns) {
128
125
  (key) => `A "${returns}" tool response needs a "${key}" key; the host draws the card from it.`
129
126
  );
130
127
  }
131
-
132
- // src/testing/work-events.ts
133
- function workCommentCreatedFixture(overrides = {}) {
134
- return {
135
- type: "comment.created",
136
- externalItemId: "item_test",
137
- providerEventId: "event_comment_test",
138
- occurredAt: "2026-10-15T12:00:00.000Z",
139
- dedupeKey: "item_test:event_comment_test",
140
- comment: {
141
- externalCommentId: "comment_test",
142
- body: "A provider comment.",
143
- author: { id: "author_test", displayName: "Sam" },
144
- createdAt: "2026-10-15T12:00:00.000Z"
145
- },
146
- ...overrides
147
- };
148
- }
149
- function workStatusChangedFixture(overrides = {}) {
150
- return {
151
- type: "status.changed",
152
- externalItemId: "item_test",
153
- providerEventId: "event_status_test",
154
- occurredAt: "2026-10-15T12:00:00.000Z",
155
- dedupeKey: "item_test:event_status_test",
156
- status: { id: "resolved_test", label: "Resolved", stage: "done" },
157
- ...overrides
158
- };
159
- }
160
- function workItemDeletedFixture(overrides = {}) {
161
- return {
162
- type: "item.deleted",
163
- externalItemId: "item_test",
164
- providerEventId: "event_deleted_test",
165
- occurredAt: "2026-10-15T12:00:00.000Z",
166
- dedupeKey: "item_test:event_deleted_test",
167
- ...overrides
168
- };
169
- }
170
- async function signedWorkEventRequest(event, options) {
171
- let request;
172
- await sendWorkEvent(event, {
173
- ...options,
174
- fetch: (input, init) => {
175
- request = new Request(input, init);
176
- return Promise.resolve(new Response(null, { status: 202 }));
177
- }
178
- });
179
- if (!request)
180
- throw new Error("The work event sender did not produce a request.");
181
- return request;
182
- }
183
128
  export {
184
129
  answerFixtureQuery,
185
130
  createTapMock,
@@ -198,13 +143,9 @@ export {
198
143
  registerTapMatchers,
199
144
  renderWithTap,
200
145
  signTestToken,
201
- signedWorkEventRequest,
202
146
  tapMatchers,
203
147
  testJwks,
204
148
  validateMetricsResponse,
205
- validateToolResponse,
206
- workCommentCreatedFixture,
207
- workItemDeletedFixture,
208
- workStatusChangedFixture
149
+ validateToolResponse
209
150
  };
210
151
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing/render.tsx","../../src/testing/validators.ts","../../src/testing/work-events.ts"],"sourcesContent":["import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport {\n render,\n type RenderOptions,\n type RenderResult,\n} from '@testing-library/react';\nimport type { ReactElement, ReactNode } from 'react';\nimport { installPrefix, procedurePrefix } from '../client/query';\nimport type { Tap } from '../client/types';\nimport { TapHostProvider } from '../host/provider';\nimport { createTapMock, type CreateTapMockOptions, type TapMock } from './mock';\n\n/**\n * What `renderWithTap` takes: its own three options, plus every\n * `CreateTapMockOptions` member it forwards to the mock.\n */\nexport interface RenderWithTapOptions extends CreateTapMockOptions {\n /** A mock you built yourself. Given, every mock option here is ignored. */\n mock?: TapMock;\n /** Your own client, for a test that reads the cache. Defaults to a fresh one. */\n queryClient?: QueryClient;\n /** Passed to Testing Library's `render`, for a mount point of your own. */\n container?: RenderOptions['container'];\n}\n\n/** What `renderWithTap` returns: Testing Library's result plus the bridge. */\nexport interface RenderWithTapResult extends RenderResult {\n /** The same object `useTap` returns inside the component. */\n tap: Tap;\n /** The mock behind the bridge, which the matchers take. */\n mock: TapMock;\n /** The client the render used, whether it was passed in or built here. */\n queryClient: QueryClient;\n}\n\n/**\n * Renders a component against the mock bridge, so a widget, tab, page, row action\n * or settings panel is testable outside Tappify.\n *\n * @remarks\n * It builds a mock from the same options `createTapMock` takes unless you pass\n * `mock`, wraps the component in the host provider and a `QueryClient` that does\n * not retry and does not cache between tests, and appends the mock's portal to the\n * rendered document so SDK dialogs have somewhere to go. It rewires\n * `tap.invalidate` onto that client, so a component calling it drops the right\n * cache entries. Scopes default to all of them, so pass `scopes` to cover the\n * refused path; a `handler` has to be built with `jwks: await testJwks()` and the\n * same `extensionId` the mock reports. Load\n * `@tappify/extension-sdk/testing/vitest` or `.../testing/jest` as a setup file\n * first: that installs the host CSS variables, registers the matchers and clears\n * the portal between tests.\n *\n * @example\n * ```tsx\n * import { useTapQuery } from '@tappify/extension-sdk';\n * import { Metric } from '@tappify/extension-sdk/ui';\n * import { renderWithTap } from '@tappify/extension-sdk/testing';\n * import { screen, waitFor } from '@testing-library/react';\n * import { expect, it } from 'vitest';\n *\n * function Crashes() {\n * const crashes = useTapQuery({\n * kind: 'crashes',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * return <Metric label=\"Issues\" value={crashes.data?.issues.length ?? 0} />;\n * }\n *\n * it('renders the issues the fixtures answer with', async () => {\n * renderWithTap(<Crashes />, { scopes: ['ui:render', 'crashes:read'] });\n * await waitFor(() => expect(screen.getByText('2')).toBeTruthy());\n * });\n * ```\n */\nexport function renderWithTap(\n ui: ReactElement,\n options: RenderWithTapOptions = {},\n): RenderWithTapResult {\n const mock = options.mock ?? createTapMock(options);\n const queryClient =\n options.queryClient ??\n new QueryClient({\n defaultOptions: { queries: { retry: false, gcTime: 0 } },\n });\n\n const installId = mock.tap.extension.installId;\n mock.tap.invalidate = name => {\n void queryClient.invalidateQueries({\n queryKey:\n name === undefined\n ? installPrefix(installId)\n : procedurePrefix(installId, name),\n });\n };\n\n function Wrapper({ children }: { children: ReactNode }): ReactElement {\n return (\n <QueryClientProvider client={queryClient}>\n <TapHostProvider tap={mock.tap}>{children}</TapHostProvider>\n </QueryClientProvider>\n );\n }\n\n const result = render(ui, { wrapper: Wrapper, container: options.container });\n result.baseElement.append(mock.portal);\n\n return { ...result, tap: mock.tap, mock, queryClient };\n}\n","import { CARD_KEYS } from '../manifest/constants';\nimport type { MetricDeclaration, ToolDeclaration } from '../manifest/types';\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return typeof value === 'object' && value !== null\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction list(value: unknown): unknown[] | null {\n return Array.isArray(value) ? value : null;\n}\n\nfunction isPoint(value: unknown): boolean {\n const pair = list(value);\n return (\n pair !== null &&\n pair.length === 2 &&\n typeof pair[0] === 'string' &&\n typeof pair[1] === 'number'\n );\n}\n\n/**\n * Checks a metrics response against the metrics the manifest declares and returns\n * what is wrong with it.\n *\n * @remarks\n * Returns an empty array when the response is sound, so a test asserts on an empty\n * list. It reports a body that is not `{ series: [...] }`, an entry with no\n * `metric` key, a metric the declarations do not name, a unit that disagrees with\n * the declared one, a missing `points` array, and any point that is not a\n * `[string, number]` pair. It does not check that every declared metric is\n * answered, so assert on the length yourself when that matters. The strings are the\n * ones `tappify extension doctor` reports.\n *\n * @example\n * ```ts\n * import { validateMetricsResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * const metrics = [\n * { key: 'orders', label: 'Orders', unit: 'count' as const, kind: 'counter' as const },\n * ];\n *\n * it('answers orders as a counted series', () => {\n * const response = { series: [{ metric: 'orders', unit: 'count', points: [['2026-09-01', 12]] }] };\n * expect(validateMetricsResponse(response, metrics)).toEqual([]);\n * });\n * ```\n */\nexport function validateMetricsResponse(\n response: unknown,\n metrics: MetricDeclaration[],\n): string[] {\n const series = list(record(response)?.series);\n\n if (series === null) {\n return [\n 'A metrics response is { \"series\": [...] }. Return one entry per declared metric, even when it has no points.',\n ];\n }\n\n const issues: string[] = [];\n const declared = new Map(metrics.map(metric => [metric.key, metric]));\n\n for (const [index, entry] of series.entries()) {\n const item = record(entry);\n const key = typeof item?.metric === 'string' ? item.metric : null;\n\n if (item === null || key === null) {\n issues.push(`series[${String(index)}] has no \"metric\" key.`);\n continue;\n }\n\n const metric = declared.get(key);\n if (metric === undefined) {\n issues.push(\n `series[${String(index)}] reports \"${key}\", which is not declared under contributes.connector.metrics.`,\n );\n continue;\n }\n\n if (item.unit !== metric.unit) {\n issues.push(\n `series[${String(index)}] reports unit \"${String(item.unit)}\" but \"${key}\" is declared as \"${metric.unit}\".`,\n );\n }\n\n const points = list(item.points);\n if (points === null) {\n issues.push(`series[${String(index)}] has no \"points\" array.`);\n continue;\n }\n\n for (const [pointIndex, point] of points.entries()) {\n if (!isPoint(point)) {\n issues.push(\n `series[${String(index)}].points[${String(pointIndex)}] must be a two-element [timestamp, value] pair: a string and a number.`,\n );\n }\n }\n }\n\n return issues;\n}\n\n/**\n * Checks a tool's answer against the card shape its declared `returns` kind has to\n * carry, and returns what is missing.\n *\n * @remarks\n * Returns an empty array when the answer is sound. A `value` card needs `label` and\n * `value`, a `series` card `label` and `points`, a `table` card `columns` and\n * `rows`, a `list` card `items`, and a `comparison` card `label`, `left` and\n * `right`; anything that is not an object is reported on its own. It checks the\n * keys are present, not what they hold, so an empty `rows` array passes.\n *\n * @example\n * ```ts\n * import { validateToolResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * it('answers keyword_gaps with a table card', () => {\n * const card = { columns: [{ key: 'term', label: 'Term' }], rows: [] };\n * expect(validateToolResponse(card, 'table')).toEqual([]);\n * });\n * ```\n */\nexport function validateToolResponse(\n value: unknown,\n returns: ToolDeclaration['returns'],\n): string[] {\n const body = record(value);\n if (body === null) {\n return [\n `A \"${returns}\" tool returns an object the host draws as a card, not ${typeof value}.`,\n ];\n }\n\n return CARD_KEYS[returns]\n .filter(key => !(key in body))\n .map(\n key =>\n `A \"${returns}\" tool response needs a \"${key}\" key; the host draws the card from it.`,\n );\n}\n","import type {\n WorkHydratedCommentCreatedEvent,\n WorkHydratedItemDeletedEvent,\n WorkHydratedStatusChangedEvent,\n} from '../server/work';\nimport {\n sendWorkEvent,\n type SendWorkEventOptions,\n type WorkEvent,\n} from '../server/work-events';\n\n/** A fresh complete comment signal with deterministic identity and shallow overrides. */\nexport function workCommentCreatedFixture(\n overrides: Partial<WorkHydratedCommentCreatedEvent> = {},\n): WorkHydratedCommentCreatedEvent {\n return {\n type: 'comment.created',\n externalItemId: 'item_test',\n providerEventId: 'event_comment_test',\n occurredAt: '2026-10-15T12:00:00.000Z',\n dedupeKey: 'item_test:event_comment_test',\n comment: {\n externalCommentId: 'comment_test',\n body: 'A provider comment.',\n author: { id: 'author_test', displayName: 'Sam' },\n createdAt: '2026-10-15T12:00:00.000Z',\n },\n ...overrides,\n };\n}\n\n/** A fresh complete status signal with deterministic identity and shallow overrides. */\nexport function workStatusChangedFixture(\n overrides: Partial<WorkHydratedStatusChangedEvent> = {},\n): WorkHydratedStatusChangedEvent {\n return {\n type: 'status.changed',\n externalItemId: 'item_test',\n providerEventId: 'event_status_test',\n occurredAt: '2026-10-15T12:00:00.000Z',\n dedupeKey: 'item_test:event_status_test',\n status: { id: 'resolved_test', label: 'Resolved', stage: 'done' },\n ...overrides,\n };\n}\n\n/** A fresh deletion signal with deterministic identity and shallow overrides. */\nexport function workItemDeletedFixture(\n overrides: Partial<WorkHydratedItemDeletedEvent> = {},\n): WorkHydratedItemDeletedEvent {\n return {\n type: 'item.deleted',\n externalItemId: 'item_test',\n providerEventId: 'event_deleted_test',\n occurredAt: '2026-10-15T12:00:00.000Z',\n dedupeKey: 'item_test:event_deleted_test',\n ...overrides,\n };\n}\n\n/**\n * Captures a signed work-event Request through the real sender without network I/O.\n *\n * @remarks\n * Uses the same validation, serialization, signing and callback URL as\n * sendWorkEvent. Supply the callback and secret your test install was configured\n * with; the accepting transport is supplied by this helper.\n */\nexport async function signedWorkEventRequest(\n event: WorkEvent,\n options: Omit<SendWorkEventOptions, 'fetch'>,\n): Promise<Request> {\n let request: Request | undefined;\n await sendWorkEvent(event, {\n ...options,\n fetch: (input, init) => {\n request = new Request(input, init);\n return Promise.resolve(new Response(null, { status: 202 }));\n },\n });\n if (!request)\n throw new Error('The work event sender did not produce a request.');\n return request;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,aAAa,2BAA2B;AACjD;AAAA,EACE;AAAA,OAGK;AA6FC;AAxBD,SAAS,cACd,IACA,UAAgC,CAAC,GACZ;AACrB,QAAM,OAAO,QAAQ,QAAQ,cAAc,OAAO;AAClD,QAAM,cACJ,QAAQ,eACR,IAAI,YAAY;AAAA,IACd,gBAAgB,EAAE,SAAS,EAAE,OAAO,OAAO,QAAQ,EAAE,EAAE;AAAA,EACzD,CAAC;AAEH,QAAM,YAAY,KAAK,IAAI,UAAU;AACrC,OAAK,IAAI,aAAa,UAAQ;AAC5B,SAAK,YAAY,kBAAkB;AAAA,MACjC,UACE,SAAS,SACL,cAAc,SAAS,IACvB,gBAAgB,WAAW,IAAI;AAAA,IACvC,CAAC;AAAA,EACH;AAEA,WAAS,QAAQ,EAAE,SAAS,GAA0C;AACpE,WACE,oBAAC,uBAAoB,QAAQ,aAC3B,8BAAC,mBAAgB,KAAK,KAAK,KAAM,UAAS,GAC5C;AAAA,EAEJ;AAEA,QAAM,SAAS,OAAO,IAAI,EAAE,SAAS,SAAS,WAAW,QAAQ,UAAU,CAAC;AAC5E,SAAO,YAAY,OAAO,KAAK,MAAM;AAErC,SAAO,EAAE,GAAG,QAAQ,KAAK,KAAK,KAAK,MAAM,YAAY;AACvD;;;ACxGA,SAAS,OAAO,OAAgD;AAC9D,SAAO,OAAO,UAAU,YAAY,UAAU,OACzC,QACD;AACN;AAEA,SAAS,KAAK,OAAkC;AAC9C,SAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ;AACxC;AAEA,SAAS,QAAQ,OAAyB;AACxC,QAAM,OAAO,KAAK,KAAK;AACvB,SACE,SAAS,QACT,KAAK,WAAW,KAChB,OAAO,KAAK,CAAC,MAAM,YACnB,OAAO,KAAK,CAAC,MAAM;AAEvB;AA8BO,SAAS,wBACd,UACA,SACU;AACV,QAAM,SAAS,KAAK,OAAO,QAAQ,GAAG,MAAM;AAE5C,MAAI,WAAW,MAAM;AACnB,WAAO;AAAA,MACL;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAmB,CAAC;AAC1B,QAAM,WAAW,IAAI,IAAI,QAAQ,IAAI,YAAU,CAAC,OAAO,KAAK,MAAM,CAAC,CAAC;AAEpE,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,GAAG;AAC7C,UAAM,OAAO,OAAO,KAAK;AACzB,UAAM,MAAM,OAAO,MAAM,WAAW,WAAW,KAAK,SAAS;AAE7D,QAAI,SAAS,QAAQ,QAAQ,MAAM;AACjC,aAAO,KAAK,UAAU,OAAO,KAAK,CAAC,wBAAwB;AAC3D;AAAA,IACF;AAEA,UAAM,SAAS,SAAS,IAAI,GAAG;AAC/B,QAAI,WAAW,QAAW;AACxB,aAAO;AAAA,QACL,UAAU,OAAO,KAAK,CAAC,cAAc,GAAG;AAAA,MAC1C;AACA;AAAA,IACF;AAEA,QAAI,KAAK,SAAS,OAAO,MAAM;AAC7B,aAAO;AAAA,QACL,UAAU,OAAO,KAAK,CAAC,mBAAmB,OAAO,KAAK,IAAI,CAAC,UAAU,GAAG,qBAAqB,OAAO,IAAI;AAAA,MAC1G;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,KAAK,MAAM;AAC/B,QAAI,WAAW,MAAM;AACnB,aAAO,KAAK,UAAU,OAAO,KAAK,CAAC,0BAA0B;AAC7D;AAAA,IACF;AAEA,eAAW,CAAC,YAAY,KAAK,KAAK,OAAO,QAAQ,GAAG;AAClD,UAAI,CAAC,QAAQ,KAAK,GAAG;AACnB,eAAO;AAAA,UACL,UAAU,OAAO,KAAK,CAAC,YAAY,OAAO,UAAU,CAAC;AAAA,QACvD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAwBO,SAAS,qBACd,OACA,SACU;AACV,QAAM,OAAO,OAAO,KAAK;AACzB,MAAI,SAAS,MAAM;AACjB,WAAO;AAAA,MACL,MAAM,OAAO,0DAA0D,OAAO,KAAK;AAAA,IACrF;AAAA,EACF;AAEA,SAAO,UAAU,OAAO,EACrB,OAAO,SAAO,EAAE,OAAO,KAAK,EAC5B;AAAA,IACC,SACE,MAAM,OAAO,4BAA4B,GAAG;AAAA,EAChD;AACJ;;;ACtIO,SAAS,0BACd,YAAsD,CAAC,GACtB;AACjC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,SAAS;AAAA,MACP,mBAAmB;AAAA,MACnB,MAAM;AAAA,MACN,QAAQ,EAAE,IAAI,eAAe,aAAa,MAAM;AAAA,MAChD,WAAW;AAAA,IACb;AAAA,IACA,GAAG;AAAA,EACL;AACF;AAGO,SAAS,yBACd,YAAqD,CAAC,GACtB;AAChC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,QAAQ,EAAE,IAAI,iBAAiB,OAAO,YAAY,OAAO,OAAO;AAAA,IAChE,GAAG;AAAA,EACL;AACF;AAGO,SAAS,uBACd,YAAmD,CAAC,GACtB;AAC9B,SAAO;AAAA,IACL,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,GAAG;AAAA,EACL;AACF;AAUA,eAAsB,uBACpB,OACA,SACkB;AAClB,MAAI;AACJ,QAAM,cAAc,OAAO;AAAA,IACzB,GAAG;AAAA,IACH,OAAO,CAAC,OAAO,SAAS;AACtB,gBAAU,IAAI,QAAQ,OAAO,IAAI;AACjC,aAAO,QAAQ,QAAQ,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC,CAAC;AAAA,IAC5D;AAAA,EACF,CAAC;AACD,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,kDAAkD;AACpE,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/testing/render.tsx","../../src/testing/validators.ts"],"sourcesContent":["import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport {\n render,\n type RenderOptions,\n type RenderResult,\n} from '@testing-library/react';\nimport type { ReactElement, ReactNode } from 'react';\nimport { installPrefix, procedurePrefix } from '../client/query';\nimport type { Tap } from '../client/types';\nimport { TapHostProvider } from '../host/provider';\nimport { createTapMock, type CreateTapMockOptions, type TapMock } from './mock';\n\n/**\n * What `renderWithTap` takes: its own three options, plus every\n * `CreateTapMockOptions` member it forwards to the mock.\n */\nexport interface RenderWithTapOptions extends CreateTapMockOptions {\n /** A mock you built yourself. Given, every mock option here is ignored. */\n mock?: TapMock;\n /** Your own client, for a test that reads the cache. Defaults to a fresh one. */\n queryClient?: QueryClient;\n /** Passed to Testing Library's `render`, for a mount point of your own. */\n container?: RenderOptions['container'];\n}\n\n/** What `renderWithTap` returns: Testing Library's result plus the bridge. */\nexport interface RenderWithTapResult extends RenderResult {\n /** The same object `useTap` returns inside the component. */\n tap: Tap;\n /** The mock behind the bridge, which the matchers take. */\n mock: TapMock;\n /** The client the render used, whether it was passed in or built here. */\n queryClient: QueryClient;\n}\n\n/**\n * Renders a component against the mock bridge, so a widget, tab, page, row action\n * or settings panel is testable outside Tappify.\n *\n * @remarks\n * It builds a mock from the same options `createTapMock` takes unless you pass\n * `mock`, wraps the component in the host provider and a `QueryClient` that does\n * not retry and does not cache between tests, and appends the mock's portal to the\n * rendered document so `TapDialog` has somewhere to go. It rewires\n * `tap.invalidate` onto that client, so a component calling it drops the right\n * cache entries. Scopes default to all of them, so pass `scopes` to cover the\n * refused path; a `handler` has to be built with `jwks: await testJwks()` and the\n * same `extensionId` the mock reports. Load\n * `@tappify/extension-sdk/testing/vitest` or `.../testing/jest` as a setup file\n * first: that installs the host CSS variables, registers the matchers and clears\n * the portal between tests.\n *\n * @example\n * ```tsx\n * import { TapStat, useTapQuery } from '@tappify/extension-sdk';\n * import { renderWithTap } from '@tappify/extension-sdk/testing';\n * import { screen, waitFor } from '@testing-library/react';\n * import { expect, it } from 'vitest';\n *\n * function Crashes() {\n * const crashes = useTapQuery({\n * kind: 'crashes',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * return <TapStat label=\"Issues\" value={crashes.data?.issues.length ?? 0} />;\n * }\n *\n * it('renders the issues the fixtures answer with', async () => {\n * renderWithTap(<Crashes />, { scopes: ['ui:render', 'crashes:read'] });\n * await waitFor(() => expect(screen.getByText('2')).toBeTruthy());\n * });\n * ```\n */\nexport function renderWithTap(\n ui: ReactElement,\n options: RenderWithTapOptions = {},\n): RenderWithTapResult {\n const mock = options.mock ?? createTapMock(options);\n const queryClient =\n options.queryClient ??\n new QueryClient({\n defaultOptions: { queries: { retry: false, gcTime: 0 } },\n });\n\n const installId = mock.tap.extension.installId;\n mock.tap.invalidate = name => {\n void queryClient.invalidateQueries({\n queryKey:\n name === undefined\n ? installPrefix(installId)\n : procedurePrefix(installId, name),\n });\n };\n\n function Wrapper({ children }: { children: ReactNode }): ReactElement {\n return (\n <QueryClientProvider client={queryClient}>\n <TapHostProvider tap={mock.tap}>{children}</TapHostProvider>\n </QueryClientProvider>\n );\n }\n\n const result = render(ui, { wrapper: Wrapper, container: options.container });\n result.baseElement.append(mock.portal);\n\n return { ...result, tap: mock.tap, mock, queryClient };\n}\n","import { CARD_KEYS } from '../manifest/constants';\nimport type { MetricDeclaration, ToolDeclaration } from '../manifest/types';\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return typeof value === 'object' && value !== null\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction list(value: unknown): unknown[] | null {\n return Array.isArray(value) ? value : null;\n}\n\nfunction isPoint(value: unknown): boolean {\n const pair = list(value);\n return (\n pair !== null &&\n pair.length === 2 &&\n typeof pair[0] === 'string' &&\n typeof pair[1] === 'number'\n );\n}\n\n/**\n * Checks a metrics response against the metrics the manifest declares and returns\n * what is wrong with it.\n *\n * @remarks\n * Returns an empty array when the response is sound, so a test asserts on an empty\n * list. It reports a body that is not `{ series: [...] }`, an entry with no\n * `metric` key, a metric the declarations do not name, a unit that disagrees with\n * the declared one, a missing `points` array, and any point that is not a\n * `[string, number]` pair. It does not check that every declared metric is\n * answered, so assert on the length yourself when that matters. The strings are the\n * ones `tappify extension doctor` reports.\n *\n * @example\n * ```ts\n * import { validateMetricsResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * const metrics = [\n * { key: 'orders', label: 'Orders', unit: 'count' as const, kind: 'counter' as const },\n * ];\n *\n * it('answers orders as a counted series', () => {\n * const response = { series: [{ metric: 'orders', unit: 'count', points: [['2026-09-01', 12]] }] };\n * expect(validateMetricsResponse(response, metrics)).toEqual([]);\n * });\n * ```\n */\nexport function validateMetricsResponse(\n response: unknown,\n metrics: MetricDeclaration[],\n): string[] {\n const series = list(record(response)?.series);\n\n if (series === null) {\n return [\n 'A metrics response is { \"series\": [...] }. Return one entry per declared metric, even when it has no points.',\n ];\n }\n\n const issues: string[] = [];\n const declared = new Map(metrics.map(metric => [metric.key, metric]));\n\n for (const [index, entry] of series.entries()) {\n const item = record(entry);\n const key = typeof item?.metric === 'string' ? item.metric : null;\n\n if (item === null || key === null) {\n issues.push(`series[${String(index)}] has no \"metric\" key.`);\n continue;\n }\n\n const metric = declared.get(key);\n if (metric === undefined) {\n issues.push(\n `series[${String(index)}] reports \"${key}\", which is not declared under contributes.connector.metrics.`,\n );\n continue;\n }\n\n if (item.unit !== metric.unit) {\n issues.push(\n `series[${String(index)}] reports unit \"${String(item.unit)}\" but \"${key}\" is declared as \"${metric.unit}\".`,\n );\n }\n\n const points = list(item.points);\n if (points === null) {\n issues.push(`series[${String(index)}] has no \"points\" array.`);\n continue;\n }\n\n for (const [pointIndex, point] of points.entries()) {\n if (!isPoint(point)) {\n issues.push(\n `series[${String(index)}].points[${String(pointIndex)}] must be a two-element [timestamp, value] pair: a string and a number.`,\n );\n }\n }\n }\n\n return issues;\n}\n\n/**\n * Checks a tool's answer against the card shape its declared `returns` kind has to\n * carry, and returns what is missing.\n *\n * @remarks\n * Returns an empty array when the answer is sound. A `value` card needs `label` and\n * `value`, a `series` card `label` and `points`, a `table` card `columns` and\n * `rows`, a `list` card `items`, and a `comparison` card `label`, `left` and\n * `right`; anything that is not an object is reported on its own. It checks the\n * keys are present, not what they hold, so an empty `rows` array passes.\n *\n * @example\n * ```ts\n * import { validateToolResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * it('answers keyword_gaps with a table card', () => {\n * const card = { columns: [{ key: 'term', label: 'Term' }], rows: [] };\n * expect(validateToolResponse(card, 'table')).toEqual([]);\n * });\n * ```\n */\nexport function validateToolResponse(\n value: unknown,\n returns: ToolDeclaration['returns'],\n): string[] {\n const body = record(value);\n if (body === null) {\n return [\n `A \"${returns}\" tool returns an object the host draws as a card, not ${typeof value}.`,\n ];\n }\n\n return CARD_KEYS[returns]\n .filter(key => !(key in body))\n .map(\n key =>\n `A \"${returns}\" tool response needs a \"${key}\" key; the host draws the card from it.`,\n );\n}\n"],"mappings":"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","names":[]}
@@ -2,17 +2,17 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkO357OJXLcjs = require('../chunk-O357OJXL.cjs');
6
- require('../chunk-6HETNZJY.cjs');
5
+ var _chunk3YWVYA42cjs = require('../chunk-3YWVYA42.cjs');
6
+ require('../chunk-2DBSVKAU.cjs');
7
7
  require('../chunk-Q7SFCCGT.cjs');
8
8
 
9
9
  // src/testing/jest.ts
10
10
  var jestGlobal = globalThis;
11
- _chunkO357OJXLcjs.installHostTheme.call(void 0, document.documentElement);
11
+ _chunk3YWVYA42cjs.installHostTheme.call(void 0, document.documentElement);
12
12
  if (jestGlobal.expect !== void 0) {
13
- _chunkO357OJXLcjs.registerTapMatchers.call(void 0, jestGlobal.expect);
13
+ _chunk3YWVYA42cjs.registerTapMatchers.call(void 0, jestGlobal.expect);
14
14
  }
15
15
  _optionalChain([jestGlobal, 'access', _ => _.afterEach, 'optionalCall', _2 => _2(() => {
16
- _chunkO357OJXLcjs.clearTapPortals.call(void 0, );
16
+ _chunk3YWVYA42cjs.clearTapPortals.call(void 0, );
17
17
  })]);
18
18
  //# sourceMappingURL=jest.cjs.map
@@ -2,8 +2,8 @@ import {
2
2
  clearTapPortals,
3
3
  installHostTheme,
4
4
  registerTapMatchers
5
- } from "../chunk-SLGOHIBL.mjs";
6
- import "../chunk-QXD7WVKK.mjs";
5
+ } from "../chunk-IGK2AP6J.mjs";
6
+ import "../chunk-CSTYBKZC.mjs";
7
7
  import "../chunk-J5LGTIGS.mjs";
8
8
 
9
9
  // src/testing/jest.ts
@@ -2,15 +2,15 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkO357OJXLcjs = require('../chunk-O357OJXL.cjs');
6
- require('../chunk-6HETNZJY.cjs');
5
+ var _chunk3YWVYA42cjs = require('../chunk-3YWVYA42.cjs');
6
+ require('../chunk-2DBSVKAU.cjs');
7
7
  require('../chunk-Q7SFCCGT.cjs');
8
8
 
9
9
  // src/testing/vitest.ts
10
10
  var _vitest = require('vitest');
11
- _chunkO357OJXLcjs.installHostTheme.call(void 0, document.documentElement);
12
- _chunkO357OJXLcjs.registerTapMatchers.call(void 0, _vitest.expect);
11
+ _chunk3YWVYA42cjs.installHostTheme.call(void 0, document.documentElement);
12
+ _chunk3YWVYA42cjs.registerTapMatchers.call(void 0, _vitest.expect);
13
13
  _vitest.afterEach.call(void 0, () => {
14
- _chunkO357OJXLcjs.clearTapPortals.call(void 0, );
14
+ _chunk3YWVYA42cjs.clearTapPortals.call(void 0, );
15
15
  });
16
16
  //# sourceMappingURL=vitest.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/testing/vitest.cjs","../../src/testing/vitest.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACA;ACRA,gCAAkC;AA8BlC,gDAAA,QAAiB,CAAS,eAAe,CAAA;AACzC,mDAAA,cAA0B,CAAA;AAE1B,+BAAA,CAAU,EAAA,GAAM;AACd,EAAA,+CAAA,CAAgB;AAClB,CAAC,CAAA","file":"/home/runner/work/extension-sdk/extension-sdk/dist/testing/vitest.cjs","sourcesContent":[null,"import { afterEach, expect } from 'vitest';\nimport { clearTapPortals, installHostTheme } from './environment';\nimport { registerTapMatchers } from './matchers';\n\nexport type { MatcherResult } from './matchers';\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- an augmentation has to repeat vitest's own `Matchers<T = any>` parameter list verbatim */\ndeclare module 'vitest' {\n interface Matchers<T = any> {\n toHaveNavigatedTo(path: string): T;\n toHaveToasted(expected: string | RegExp): T;\n toHaveRunAction(actionId: string, input?: unknown): T;\n toHaveStored(collection: string, document: unknown): T;\n }\n}\n\n/* eslint-disable @typescript-eslint/no-namespace -- Chai exposes its matcher contract as a global namespace */\ndeclare global {\n namespace Chai {\n interface Assertion {\n toHaveNavigatedTo(path: string): void;\n toHaveToasted(expected: string | RegExp): void;\n toHaveRunAction(actionId: string, input?: unknown): void;\n toHaveStored(collection: string, document: unknown): void;\n }\n }\n}\n/* eslint-enable @typescript-eslint/no-namespace */\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\ninstallHostTheme(document.documentElement);\nregisterTapMatchers(expect);\n\nafterEach(() => {\n clearTapPortals();\n});\n"]}
1
+ {"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/testing/vitest.cjs","../../src/testing/vitest.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACA;ACRA,gCAAkC;AAiBlC,gDAAA,QAAiB,CAAS,eAAe,CAAA;AACzC,mDAAA,cAA0B,CAAA;AAE1B,+BAAA,CAAU,EAAA,GAAM;AACd,EAAA,+CAAA,CAAgB;AAClB,CAAC,CAAA","file":"/home/runner/work/extension-sdk/extension-sdk/dist/testing/vitest.cjs","sourcesContent":[null,"import { afterEach, expect } from 'vitest';\nimport { clearTapPortals, installHostTheme } from './environment';\nimport { registerTapMatchers } from './matchers';\n\nexport type { MatcherResult } from './matchers';\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- an augmentation has to repeat vitest's own `Matchers<T = any>` parameter list verbatim */\ndeclare module 'vitest' {\n interface Matchers<T = any> {\n toHaveNavigatedTo(path: string): T;\n toHaveToasted(expected: string | RegExp): T;\n toHaveRunAction(actionId: string, input?: unknown): T;\n toHaveStored(collection: string, document: unknown): T;\n }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\ninstallHostTheme(document.documentElement);\nregisterTapMatchers(expect);\n\nafterEach(() => {\n clearTapPortals();\n});\n"]}
@@ -8,13 +8,3 @@ declare module 'vitest' {
8
8
  toHaveStored(collection: string, document: unknown): T;
9
9
  }
10
10
  }
11
- declare global {
12
- namespace Chai {
13
- interface Assertion {
14
- toHaveNavigatedTo(path: string): void;
15
- toHaveToasted(expected: string | RegExp): void;
16
- toHaveRunAction(actionId: string, input?: unknown): void;
17
- toHaveStored(collection: string, document: unknown): void;
18
- }
19
- }
20
- }
@@ -8,13 +8,3 @@ declare module 'vitest' {
8
8
  toHaveStored(collection: string, document: unknown): T;
9
9
  }
10
10
  }
11
- declare global {
12
- namespace Chai {
13
- interface Assertion {
14
- toHaveNavigatedTo(path: string): void;
15
- toHaveToasted(expected: string | RegExp): void;
16
- toHaveRunAction(actionId: string, input?: unknown): void;
17
- toHaveStored(collection: string, document: unknown): void;
18
- }
19
- }
20
- }
@@ -2,8 +2,8 @@ import {
2
2
  clearTapPortals,
3
3
  installHostTheme,
4
4
  registerTapMatchers
5
- } from "../chunk-SLGOHIBL.mjs";
6
- import "../chunk-QXD7WVKK.mjs";
5
+ } from "../chunk-IGK2AP6J.mjs";
6
+ import "../chunk-CSTYBKZC.mjs";
7
7
  import "../chunk-J5LGTIGS.mjs";
8
8
 
9
9
  // src/testing/vitest.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing/vitest.ts"],"sourcesContent":["import { afterEach, expect } from 'vitest';\nimport { clearTapPortals, installHostTheme } from './environment';\nimport { registerTapMatchers } from './matchers';\n\nexport type { MatcherResult } from './matchers';\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- an augmentation has to repeat vitest's own `Matchers<T = any>` parameter list verbatim */\ndeclare module 'vitest' {\n interface Matchers<T = any> {\n toHaveNavigatedTo(path: string): T;\n toHaveToasted(expected: string | RegExp): T;\n toHaveRunAction(actionId: string, input?: unknown): T;\n toHaveStored(collection: string, document: unknown): T;\n }\n}\n\n/* eslint-disable @typescript-eslint/no-namespace -- Chai exposes its matcher contract as a global namespace */\ndeclare global {\n namespace Chai {\n interface Assertion {\n toHaveNavigatedTo(path: string): void;\n toHaveToasted(expected: string | RegExp): void;\n toHaveRunAction(actionId: string, input?: unknown): void;\n toHaveStored(collection: string, document: unknown): void;\n }\n }\n}\n/* eslint-enable @typescript-eslint/no-namespace */\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\ninstallHostTheme(document.documentElement);\nregisterTapMatchers(expect);\n\nafterEach(() => {\n clearTapPortals();\n});\n"],"mappings":";;;;;;;;;AAAA,SAAS,WAAW,cAAc;AA8BlC,iBAAiB,SAAS,eAAe;AACzC,oBAAoB,MAAM;AAE1B,UAAU,MAAM;AACd,kBAAgB;AAClB,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/testing/vitest.ts"],"sourcesContent":["import { afterEach, expect } from 'vitest';\nimport { clearTapPortals, installHostTheme } from './environment';\nimport { registerTapMatchers } from './matchers';\n\nexport type { MatcherResult } from './matchers';\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- an augmentation has to repeat vitest's own `Matchers<T = any>` parameter list verbatim */\ndeclare module 'vitest' {\n interface Matchers<T = any> {\n toHaveNavigatedTo(path: string): T;\n toHaveToasted(expected: string | RegExp): T;\n toHaveRunAction(actionId: string, input?: unknown): T;\n toHaveStored(collection: string, document: unknown): T;\n }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\ninstallHostTheme(document.documentElement);\nregisterTapMatchers(expect);\n\nafterEach(() => {\n clearTapPortals();\n});\n"],"mappings":";;;;;;;;;AAAA,SAAS,WAAW,cAAc;AAiBlC,iBAAiB,SAAS,eAAe;AACzC,oBAAoB,MAAM;AAE1B,UAAU,MAAM;AACd,kBAAgB;AAClB,CAAC;","names":[]}
@@ -1,4 +1,4 @@
1
- import { S as ScopeKey, H as HostEventName } from './constants-BAimnqOK.js';
1
+ import { S as ScopeKey, H as HostEventName } from './constants-DJ_KgZpN.js';
2
2
 
3
3
  /**
4
4
  * The storage collections the manifest declares, keyed by collection name, which
@@ -556,12 +556,11 @@ interface TapInternals {
556
556
  *
557
557
  * @example
558
558
  * ```tsx
559
- * import { useTap } from '@tappify/extension-sdk';
560
- * import { Metric } from '@tappify/extension-sdk/ui';
559
+ * import { TapStat, useTap } from '@tappify/extension-sdk';
561
560
  *
562
561
  * function Installs({ count }: { count: number }) {
563
562
  * const tap = useTap();
564
- * return <Metric label="Installs" value={tap.format.number(count)} />;
563
+ * return <TapStat label="Installs" value={tap.format.number(count)} />;
565
564
  * }
566
565
  * ```
567
566
  */
@@ -1,4 +1,4 @@
1
- import { S as ScopeKey, H as HostEventName } from './constants-BAimnqOK.cjs';
1
+ import { S as ScopeKey, H as HostEventName } from './constants-DJ_KgZpN.cjs';
2
2
 
3
3
  /**
4
4
  * The storage collections the manifest declares, keyed by collection name, which
@@ -556,12 +556,11 @@ interface TapInternals {
556
556
  *
557
557
  * @example
558
558
  * ```tsx
559
- * import { useTap } from '@tappify/extension-sdk';
560
- * import { Metric } from '@tappify/extension-sdk/ui';
559
+ * import { TapStat, useTap } from '@tappify/extension-sdk';
561
560
  *
562
561
  * function Installs({ count }: { count: number }) {
563
562
  * const tap = useTap();
564
- * return <Metric label="Installs" value={tap.format.number(count)} />;
563
+ * return <TapStat label="Installs" value={tap.format.number(count)} />;
565
564
  * }
566
565
  * ```
567
566
  */