@ekanos/harness 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -738,6 +738,12 @@ Every invocation runs against **one** `createMockContext()` per visit, so state
738
738
  accumulates across invocations the way it would in a real account. Leaving the
739
739
  page resets it.
740
740
 
741
+ **`onActivate` runs on the Activation surface, not here.** Submitting the
742
+ activation form there runs your real `onActivate` hook (if declared) against
743
+ its own `createMockContext()`, seeded from the same `triggerMocks` below, and
744
+ shows the outcome — ran / skipped (not declared) / threw with the message —
745
+ right under the connect dialog.
746
+
741
747
  ### `triggerMocks`
742
748
 
743
749
  The mock context is derived from the definition — slug, storage schemas, egress
@@ -0,0 +1,14 @@
1
+ import { type MockIntegrationContext } from '@ekanos/sdk/testing';
2
+ import { type FixtureVariant, type HarnessIntegration } from '../../registry.js';
3
+ /**
4
+ * One `createMockContext()` derived from a harness integration's definition
5
+ * (slug, storage schemas, egress) plus its `triggerMocks` seeds — the exact
6
+ * construction the Triggers surface uses for webhooks/schedules, extracted
7
+ * here so the Activation surface can run `onActivate` against the SAME kind
8
+ * of context a partner's handlers see elsewhere in the harness.
9
+ *
10
+ * One context per mount: `useState`'s initializer keeps it stable across
11
+ * re-renders without an effect, and state (storage, secrets, logs)
12
+ * accumulates for the life of the visit, like a real account.
13
+ */
14
+ export declare function useDefinitionMockContext(integration: HarnessIntegration, variant: FixtureVariant): MockIntegrationContext | null;
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import { useState } from "react";
3
+ import {
4
+ createMockContext
5
+ } from "@ekanos/sdk/testing";
6
+ import {
7
+ HARNESS_ACCOUNT_ID,
8
+ resolveEgress
9
+ } from "../../registry.js";
10
+ import { compileFixtures, fixturesToMockHandlers } from "./http-fixtures.js";
11
+ function useDefinitionMockContext(integration, variant) {
12
+ const [ctx] = useState(() => {
13
+ const definition = integration.definition;
14
+ if (!definition) return null;
15
+ const mocks = integration.triggerMocks ?? {};
16
+ return createMockContext({
17
+ accountId: HARNESS_ACCOUNT_ID,
18
+ integration: { slug: definition.slug },
19
+ ...definition.storage ? { storageSchemas: definition.storage } : {},
20
+ egress: definition.egress ?? [],
21
+ ...mocks.storage ? {
22
+ storage: mocks.storage
23
+ } : {},
24
+ ...mocks.secrets ? { secrets: mocks.secrets } : {},
25
+ fetchHandlers: [
26
+ ...mocks.fetchHandlers ?? [],
27
+ ...fixturesToMockHandlers(
28
+ // Client-only, like the layout: no origin on the server, and
29
+ // nothing fetches there.
30
+ globalThis.location === void 0 ? [] : compileFixtures(
31
+ integration.fixtures?.[variant] ?? [],
32
+ resolveEgress(integration),
33
+ globalThis.location.origin
34
+ )
35
+ )
36
+ ]
37
+ });
38
+ });
39
+ return ctx;
40
+ }
41
+ export {
42
+ useDefinitionMockContext
43
+ };
@@ -0,0 +1,28 @@
1
+ import type { IntegrationDefinition } from '@ekanos/sdk/integration';
2
+ import { type MockIntegrationContext } from '@ekanos/sdk/testing';
3
+ /**
4
+ * The outcome of one local run of a definition's `onActivate` hook, for the
5
+ * Activation surface's readout:
6
+ *
7
+ * - `'skipped'` — the definition (or the harness entry) declares no
8
+ * `onActivate` at all. Not an error: most integrations have nothing to
9
+ * seed and this is a perfectly good answer.
10
+ * - `'ran'` — the hook completed without throwing.
11
+ * - `'threw'` — the hook threw. In production this is NON-FATAL (logged
12
+ * as a warning, activation stays active — see `OnActivateHandler`'s
13
+ * TSDoc); the harness surfaces the same "not fatal, but look at this"
14
+ * framing rather than treating it as a failed activation.
15
+ *
16
+ * Pulled out of the activation route component so it is testable without
17
+ * rendering anything — the harness deliberately carries no component/DOM
18
+ * tests (see vitest.config.ts), only the underlying logic.
19
+ */
20
+ export type OnActivateOutcome = {
21
+ status: 'skipped';
22
+ } | {
23
+ status: 'ran';
24
+ } | {
25
+ status: 'threw';
26
+ message: string;
27
+ };
28
+ export declare function runOnActivateHook(definition: IntegrationDefinition | undefined, ctx: MockIntegrationContext | null): Promise<OnActivateOutcome>;
@@ -0,0 +1,20 @@
1
+ import {
2
+ invokeActivate
3
+ } from "@ekanos/sdk/testing";
4
+ function describeError(error) {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
7
+ async function runOnActivateHook(definition, ctx) {
8
+ if (!definition?.onActivate || !ctx) {
9
+ return { status: "skipped" };
10
+ }
11
+ try {
12
+ await invokeActivate(definition, { context: ctx });
13
+ return { status: "ran" };
14
+ } catch (error) {
15
+ return { status: "threw", message: describeError(error) };
16
+ }
17
+ }
18
+ export {
19
+ runOnActivateHook
20
+ };
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { notFound } from "next/navigation";
5
5
  import { Alert, AlertDescription, AlertTitle } from "@ekanos/ui/alert";
@@ -13,11 +13,27 @@ import {
13
13
  DialogTitle
14
14
  } from "@ekanos/ui/dialog";
15
15
  import { HARNESS_ACCOUNT_ID } from "../../registry.js";
16
+ import { useDefinitionMockContext } from "../lib/definition-mock-context.js";
16
17
  import { hasSensitiveValues, redactSensitive } from "../lib/redact.js";
18
+ import {
19
+ runOnActivateHook
20
+ } from "../lib/run-activation-hook.js";
21
+ import { useToolbar } from "../lib/toolbar-context.js";
17
22
  import { useHarnessIntegration } from "../registry-context.js";
18
23
  function ActivationPage() {
19
- const [flow, setFlow] = useState({ open: true, connected: false, result: null, revealed: false });
24
+ const [flow, setFlow] = useState({
25
+ open: true,
26
+ connected: false,
27
+ result: null,
28
+ revealed: false,
29
+ activationHook: null
30
+ });
20
31
  const integration = useHarnessIntegration();
32
+ const { state: toolbar } = useToolbar();
33
+ const ctx = useDefinitionMockContext(
34
+ integration ?? { slug: "", name: "", description: "", widgets: [] },
35
+ toolbar.variant
36
+ );
21
37
  if (!integration) notFound();
22
38
  const ActivationForm = integration.activationForm;
23
39
  const definition = integration.definition;
@@ -67,7 +83,8 @@ function ActivationPage() {
67
83
  open: false,
68
84
  connected: false,
69
85
  result: null,
70
- revealed: false
86
+ revealed: false,
87
+ activationHook: null
71
88
  }),
72
89
  children: "Reset flow"
73
90
  }
@@ -98,7 +115,8 @@ function ActivationPage() {
98
115
  open: true,
99
116
  connected: false,
100
117
  result: null,
101
- revealed: false
118
+ revealed: false,
119
+ activationHook: null
102
120
  })
103
121
  }
104
122
  ) : (
@@ -113,12 +131,19 @@ function ActivationPage() {
113
131
  accountId: HARNESS_ACCOUNT_ID,
114
132
  productSlug: integration.slug,
115
133
  inline: true,
116
- onSuccess: (result) => setFlow({
117
- open: false,
118
- connected: true,
119
- result: result ?? null,
120
- revealed: false
121
- }),
134
+ onSuccess: async (result) => {
135
+ const activationHook = await runOnActivateHook(
136
+ definition,
137
+ ctx
138
+ );
139
+ setFlow({
140
+ open: false,
141
+ connected: true,
142
+ result: result ?? null,
143
+ revealed: false,
144
+ activationHook
145
+ });
146
+ },
122
147
  onCancel: () => setFlow((prev) => ({ ...prev, open: false }))
123
148
  }
124
149
  )
@@ -174,7 +199,34 @@ function ActivationPage() {
174
199
  null,
175
200
  2
176
201
  ) })
177
- ] }) : null
202
+ ] }) : null,
203
+ flow.activationHook ? /* @__PURE__ */ jsx(OnActivatePanel, { outcome: flow.activationHook }) : null
204
+ ]
205
+ }
206
+ );
207
+ }
208
+ function OnActivatePanel({ outcome }) {
209
+ const badge = outcome.status === "ran" ? { variant: "default", label: "onActivate ran" } : outcome.status === "skipped" ? {
210
+ variant: "secondary",
211
+ label: "onActivate skipped \u2014 not declared"
212
+ } : { variant: "destructive", label: "onActivate threw" };
213
+ return /* @__PURE__ */ jsxs(
214
+ "div",
215
+ {
216
+ className: "bg-card flex flex-col gap-2 rounded-lg border p-6",
217
+ "data-test": "activation-onactivate-outcome",
218
+ children: [
219
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx(Badge, { variant: badge.variant, children: badge.label }) }),
220
+ outcome.status === "skipped" ? /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-sm", children: [
221
+ "This integration declares no ",
222
+ /* @__PURE__ */ jsx("code", { children: "onActivate" }),
223
+ " hook \u2014 a perfectly good answer for one with nothing to seed at connect time. Add one to ",
224
+ /* @__PURE__ */ jsx("code", { children: "defineIntegration()" }),
225
+ " to warm a cache or sanity-check a credential the moment a user connects."
226
+ ] }) : outcome.status === "threw" ? /* @__PURE__ */ jsxs(Fragment, { children: [
227
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "In production this is NON-FATAL: the throw is logged and shown to the user as a warning, and the activation stays connected \u2014 this hook is for cache seeding and eager validation, not a connect gate." }),
228
+ /* @__PURE__ */ jsx("pre", { className: "bg-muted overflow-x-auto rounded-md p-3 text-xs", children: outcome.message })
229
+ ] }) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Ran against the same kind of capability context (storage, secrets, egress) your schedules and webhooks run against." })
178
230
  ]
179
231
  }
180
232
  );
@@ -4,7 +4,6 @@ import { useState } from "react";
4
4
  import { notFound } from "next/navigation";
5
5
  import { isEgressAllowed } from "@ekanos/sdk/context";
6
6
  import {
7
- createMockContext,
8
7
  invokeSchedule,
9
8
  invokeWebhook
10
9
  } from "@ekanos/sdk/testing";
@@ -12,43 +11,9 @@ import { Alert, AlertDescription, AlertTitle } from "@ekanos/ui/alert";
12
11
  import { Badge } from "@ekanos/ui/badge";
13
12
  import { Button } from "@ekanos/ui/button";
14
13
  import { Textarea } from "@ekanos/ui/textarea";
15
- import {
16
- HARNESS_ACCOUNT_ID,
17
- resolveEgress
18
- } from "../../registry.js";
19
- import { compileFixtures, fixturesToMockHandlers } from "../lib/http-fixtures.js";
14
+ import { useDefinitionMockContext } from "../lib/definition-mock-context.js";
20
15
  import { useToolbar } from "../lib/toolbar-context.js";
21
16
  import { useHarnessIntegration } from "../registry-context.js";
22
- function useTriggerContext(integration, variant) {
23
- const [ctx] = useState(() => {
24
- const definition = integration.definition;
25
- if (!definition) return null;
26
- const mocks = integration.triggerMocks ?? {};
27
- return createMockContext({
28
- accountId: HARNESS_ACCOUNT_ID,
29
- integration: { slug: definition.slug },
30
- ...definition.storage ? { storageSchemas: definition.storage } : {},
31
- egress: definition.egress ?? [],
32
- ...mocks.storage ? {
33
- storage: mocks.storage
34
- } : {},
35
- ...mocks.secrets ? { secrets: mocks.secrets } : {},
36
- fetchHandlers: [
37
- ...mocks.fetchHandlers ?? [],
38
- ...fixturesToMockHandlers(
39
- // Client-only, like the layout: no origin on the server, and
40
- // nothing fetches there.
41
- globalThis.location === void 0 ? [] : compileFixtures(
42
- integration.fixtures?.[variant] ?? [],
43
- resolveEgress(integration),
44
- globalThis.location.origin
45
- )
46
- )
47
- ]
48
- });
49
- });
50
- return ctx;
51
- }
52
17
  function OutcomePanel({ outcome }) {
53
18
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", "data-test": "trigger-outcome", children: [
54
19
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
@@ -320,7 +285,7 @@ function OAuthCard({ definition }) {
320
285
  function TriggersPage() {
321
286
  const integration = useHarnessIntegration();
322
287
  const { state } = useToolbar();
323
- const ctx = useTriggerContext(
288
+ const ctx = useDefinitionMockContext(
324
289
  integration ?? { slug: "", name: "", description: "", widgets: [] },
325
290
  state.variant
326
291
  );
package/dist/styles.css CHANGED
@@ -161,8 +161,12 @@ i:is(.fa-light, .fa-thin, .fa-duotone, .fa-sharp) {
161
161
  * below and the authored Pro glyph renders exactly as designed. Nothing here
162
162
  * changes what a correctly licensed host shows.
163
163
  *
164
- * Keep this list to names our own bundled examples actually reference; it is a
165
- * courtesy for the demo surfaces, not a general-purpose Pro→Free shim. */
164
+ * Keep this list to names our own bundled examples actually reference, or a
165
+ * real Pro-only name a partner build hit and reported (the block below it) —
166
+ * it is a courtesy for demo/partner surfaces, not a general-purpose
167
+ * Pro→Free shim covering every Pro glyph. `src/_impl/__tests__/
168
+ * fa-free-icon-names.test.tsx` in `@ekanos/ui` asserts every name here is
169
+ * genuinely Free-missing and every codepoint names a real Free glyph. */
166
170
  i.fa-sun-bright {
167
171
  --fa: '\f185'; /* fa-sun */
168
172
  }
@@ -194,6 +198,37 @@ i.fa-sparkles {
194
198
  --fa: '\e2ca'; /* fa-wand-magic-sparkles */
195
199
  }
196
200
 
201
+ /* Reported by a partner build (2026-09-08): all seven are real Font Awesome
202
+ * PRO names (verified against packages/ui/fontawesome) that Free does not
203
+ * ship, so every one of them was rendering the bare circle-question fallback
204
+ * with no indication why. `fa-circle-question` itself was also reported, but
205
+ * it is NOT Free-missing — Free ships it natively at the same codepoint
206
+ * (`\f059`) the fallback rule above uses, so a probe that treats "content
207
+ * resolved to \f059" as "broken" cannot tell a genuine circle-question icon
208
+ * apart from the fallback. That is a probe-methodology caveat, not a shim
209
+ * this file can fix. */
210
+ i.fa-cloud-snow {
211
+ --fa: '\f2dc'; /* fa-snowflake */
212
+ }
213
+ i.fa-cloud-hail {
214
+ --fa: '\f73b'; /* fa-cloud-meatball */
215
+ }
216
+ i.fa-cloud-hail-mixed {
217
+ --fa: '\f73b'; /* fa-cloud-meatball */
218
+ }
219
+ i.fa-fog {
220
+ --fa: '\f75f'; /* fa-smog */
221
+ }
222
+ i.fa-sun-cloud {
223
+ --fa: '\f6c4'; /* fa-cloud-sun */
224
+ }
225
+ i.fa-moon-cloud {
226
+ --fa: '\f6c3'; /* fa-cloud-moon */
227
+ }
228
+ i.fa-cloud-bolt-sun {
229
+ --fa: '\f76c'; /* fa-cloud-bolt */
230
+ }
231
+
197
232
  @layer base {
198
233
  body {
199
234
  @apply bg-background text-foreground;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekanos/harness",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "The Ekanos integration dev harness — every surface of an integration rendered in real Fusion chrome, from fixtures, with no Supabase, auth or network.",
6
6
  "license": "MIT",
@@ -47,14 +47,14 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@fortawesome/fontawesome-free": "^7.3.1",
50
- "i18next": "25.10.10",
50
+ "i18next": "^25.10.10",
51
51
  "next-themes": "0.4.6",
52
52
  "react-i18next": "^16.6.6",
53
53
  "tw-animate-css": "1.4.0"
54
54
  },
55
55
  "peerDependencies": {
56
- "@ekanos/sdk": "^0.1.2",
57
- "@ekanos/ui": "^0.1.2",
56
+ "@ekanos/sdk": "^0.1.4",
57
+ "@ekanos/ui": "^0.1.4",
58
58
  "@tanstack/react-query": "^5.101.4",
59
59
  "next": "^16.0.0",
60
60
  "react": "^19.2.8",
@@ -62,8 +62,8 @@
62
62
  "tailwindcss": "^4.0.0"
63
63
  },
64
64
  "devDependencies": {
65
- "@ekanos/sdk": "0.1.2",
66
- "@ekanos/ui": "0.1.2",
65
+ "@ekanos/sdk": "0.1.4",
66
+ "@ekanos/ui": "0.1.4",
67
67
  "@kit/eslint-config": "0.2.0",
68
68
  "@kit/prettier-config": "0.1.0",
69
69
  "@kit/tsconfig": "0.1.0",