@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 +6 -0
- package/dist/internal/lib/definition-mock-context.d.ts +14 -0
- package/dist/internal/lib/definition-mock-context.js +43 -0
- package/dist/internal/lib/run-activation-hook.d.ts +28 -0
- package/dist/internal/lib/run-activation-hook.js +20 -0
- package/dist/internal/routes/activation-page.js +63 -11
- package/dist/internal/routes/triggers-page.js +2 -37
- package/dist/styles.css +37 -2
- package/package.json +6 -6
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({
|
|
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) =>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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 =
|
|
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
|
|
165
|
-
*
|
|
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.
|
|
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.
|
|
57
|
-
"@ekanos/ui": "^0.1.
|
|
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.
|
|
66
|
-
"@ekanos/ui": "0.1.
|
|
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",
|