Skip to content

Commit 59fd53f

Browse files
committed
feat: connection health checks (liveness) with OpenAPI backing
A connection can declare ONE authenticated operation that answers "is this credential still alive?". The probe runs the operation, maps the HTTP status to a health state (2xx healthy, 401/403 expired, other degraded, unset unknown), and reports it. Built for short-lived OAuth tokens (Google's 7-day dev-token revocation): a credential that authenticated yesterday and now 401s reads as expired. Core owns the shared vocabulary (HealthStatus, HealthCheckSpec, HealthCheckResult, HealthCheckCandidate, classifyHttpStatus, candidate ranking); plugins own which operation runs, stored in their opaque integration config and picked the same way auth methods are. Dispatch adds integrations.healthCheck.{get,candidates,set} and connections.{checkHealth,validate}. OpenAPI backing: list ranked candidates (non-destructive GET first), persist the chosen spec (merging over the raw config so provider supersets keep their keys), and run the probe against a resolved credential. React: a per-connection status dot + "Check now", a self-hiding operation editor (a searchable freeform combobox over the whole spec), and an optional draft on the add screen. Covered by e2e: a saved connection flips healthy -> expired when its key stops working; an unconfigured integration reports unknown; the operation picker filters a hundreds-long candidate list down to the match on both the edit sheet and the add screen.
1 parent 4ef2199 commit 59fd53f

27 files changed

Lines changed: 2156 additions & 25 deletions
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// Cross-target (browser): the UI side of connection health checks, the feature
2+
// that answers "has this credential expired?" (the Google 7-day dev-token case).
3+
// These scenarios pin the operation picker, the part that lets a user choose
4+
// WHICH call the probe runs:
5+
//
6+
// 1. Edit sheet, large spec: typing into the operation combobox filters a
7+
// hundreds-long candidate list down to the one match, and committing it
8+
// stores the real operation (not the freeform text typed to find it).
9+
// 2. Add screen, large spec: the same picker is fed by the bounded spec
10+
// preview, so typing must reach an operation ranked well past the preview's
11+
// top slice.
12+
//
13+
// These scenarios skip on targets without a browser surface (selfhost today).
14+
import { randomBytes } from "node:crypto";
15+
16+
import { Effect } from "effect";
17+
import { expect } from "@effect/vitest";
18+
import { composePluginApi } from "@executor-js/api/server";
19+
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
20+
import { IntegrationSlug } from "@executor-js/sdk/shared";
21+
22+
import { scenario } from "../src/scenario";
23+
import { Api, Browser, Target } from "../src/services";
24+
25+
const api = composePluginApi([openApiHttpPlugin()] as const);
26+
27+
const newSlug = (prefix: string) =>
28+
IntegrationSlug.make(`${prefix}-${randomBytes(4).toString("hex")}`);
29+
30+
// A distinctive operation buried in a large spec, found by its unique summary
31+
// (which no filler operation shares) so the filter test can search for it.
32+
const PROBE_TOKEN = "ztarget";
33+
const PROBE_SUMMARY = `Health probe candidate ${PROBE_TOKEN}`;
34+
35+
/** An OpenAPI 3 spec with ~250 GET operations plus one distinctive probe. The
36+
* candidate list is far longer than the popup renders at once, so the operation
37+
* picker only surfaces a given operation when typing actually filters the list.
38+
* The title is parameterizable so the add screen (which mints the slug from the
39+
* title) gets a collision-free integration per run. */
40+
const largeSpec = (baseUrl: string, title = "Big API"): string => {
41+
const okJson = {
42+
"200": {
43+
description: "ok",
44+
content: {
45+
"application/json": { schema: { type: "object", properties: { id: { type: "string" } } } },
46+
},
47+
},
48+
};
49+
const paths: Record<string, unknown> = {};
50+
for (let index = 0; index < 250; index++) {
51+
paths[`/things/item${index}`] = {
52+
get: { operationId: `getThing${index}`, summary: `Thing number ${index}`, responses: okJson },
53+
};
54+
}
55+
paths["/probe/target"] = {
56+
get: { operationId: "probeTarget", summary: PROBE_SUMMARY, responses: okJson },
57+
};
58+
return JSON.stringify({
59+
openapi: "3.0.3",
60+
info: { title, version: "1.0.0" },
61+
servers: [{ url: baseUrl }],
62+
paths,
63+
});
64+
};
65+
66+
// ===========================================================================
67+
// 1. Edit sheet, large spec: typing filters the operation picker to the match.
68+
// ===========================================================================
69+
70+
scenario(
71+
"Health checks (UI) · large spec: typing filters the operation picker down to the match",
72+
{},
73+
Effect.scoped(
74+
Effect.gen(function* () {
75+
const target = yield* Target;
76+
const browser = yield* Browser;
77+
const { client: makeClient } = yield* Api;
78+
const identity = yield* target.newIdentity();
79+
const client = yield* makeClient(api, identity);
80+
const slug = newSlug("hc-ui-large");
81+
82+
yield* Effect.ensuring(
83+
Effect.gen(function* () {
84+
yield* client.openapi.addSpec({
85+
payload: {
86+
spec: { kind: "blob", value: largeSpec("https://big.example.com") },
87+
slug,
88+
baseUrl: "https://big.example.com",
89+
authenticationTemplate: [
90+
{
91+
slug: "apiKey",
92+
type: "apiKey",
93+
headers: {
94+
authorization: ["Bearer ", { type: "variable", name: "token" }],
95+
},
96+
},
97+
],
98+
},
99+
});
100+
// The toolPath the registration assigned the distinctive probe op,
101+
// matched by its unique summary, so the read-back asserts exactly the
102+
// operation the on-camera filter-then-pick selected.
103+
const candidates = yield* client.integrations.healthCheckCandidates({ params: { slug } });
104+
const probe = candidates.find((candidate) => candidate.summary === PROBE_SUMMARY);
105+
if (!probe) return yield* Effect.die("large spec is missing its probe operation");
106+
const probeOperation = probe.operation;
107+
// Sanity: the spec really is large, so the picker can't just show them all.
108+
expect(candidates.length).toBeGreaterThan(100);
109+
110+
yield* browser.session(identity, async ({ page, step }) => {
111+
const input = page.locator("#health-check-operation");
112+
const options = page.getByRole("option");
113+
114+
await step("Open the health-check editor over the large spec", async () => {
115+
await page.goto(`/integrations/${slug}`, { waitUntil: "networkidle" });
116+
await page.getByRole("heading", { level: 3, name: "Health check" }).waitFor();
117+
await page.getByRole("button", { name: "Set up" }).click();
118+
await input.waitFor();
119+
});
120+
121+
await step("A broad query still surfaces many operations", async () => {
122+
await input.click();
123+
// Real keystrokes (base-ui filters on the input value, not a
124+
// programmatic set): a shared summary prefix matches the fillers.
125+
await input.selectText();
126+
await input.pressSequentially("Thing number", { delay: 10 });
127+
await options.filter({ hasText: "Thing number" }).first().waitFor();
128+
// The popup caps how many it renders, but a broad match fills it.
129+
expect(await options.count()).toBeGreaterThan(20);
130+
});
131+
132+
await step("A distinctive query narrows the list to the one match", async () => {
133+
await input.selectText();
134+
await input.pressSequentially(PROBE_TOKEN, { delay: 10 });
135+
const match = options.filter({ hasText: PROBE_SUMMARY }).first();
136+
await match.waitFor({ timeout: 10_000 });
137+
// Typing actually filters: the hundreds collapse to the single
138+
// matching operation (plus the freeform echo of the typed text).
139+
expect(await options.count()).toBeLessThanOrEqual(3);
140+
});
141+
142+
await step("Select the filtered operation and save", async () => {
143+
const match = options.filter({ hasText: PROBE_SUMMARY }).first();
144+
// base-ui pre-highlights the freeform echo; arrow onto the real op.
145+
for (let i = 0; i < 8; i++) {
146+
if ((await match.getAttribute("data-highlighted")) !== null) break;
147+
await input.press("ArrowDown");
148+
}
149+
await input.press("Enter");
150+
await page.getByRole("button", { name: "Save", exact: true }).click();
151+
await input.waitFor({ state: "hidden" });
152+
});
153+
});
154+
155+
// The picker committed the real operation behind the matched summary,
156+
// not the freeform text that was typed to find it.
157+
const stored = yield* client.integrations.healthCheckGet({ params: { slug } });
158+
expect(stored?.operation).toBe(probeOperation);
159+
}),
160+
Effect.gen(function* () {
161+
yield* client.openapi.removeSpec({ params: { slug } }).pipe(Effect.ignore);
162+
}),
163+
);
164+
}),
165+
),
166+
);
167+
168+
// ===========================================================================
169+
// 2. Add screen, large spec: the operation picker is fed by the bounded spec
170+
// preview, so it must carry enough of a big spec that typing reaches an
171+
// operation ranked well past the preview's top slice (the Vercel "user"
172+
// case: searching found nothing because the op wasn't in the top few).
173+
// ===========================================================================
174+
175+
scenario(
176+
"Health checks (UI) · add screen large spec: typing reaches an operation beyond the preview's top slice",
177+
{},
178+
Effect.scoped(
179+
Effect.gen(function* () {
180+
const target = yield* Target;
181+
const browser = yield* Browser;
182+
const { client: makeClient } = yield* Api;
183+
const identity = yield* target.newIdentity();
184+
const client = yield* makeClient(api, identity);
185+
// The add screen mints the slug from the title, so make it unique. Search
186+
// for an operation whose toolPath sorts far past the old top-10 cap.
187+
const title = `Big API ${randomBytes(4).toString("hex")}`;
188+
const spec = largeSpec("https://big.example.com", title);
189+
const targetSummary = "Thing number 137";
190+
191+
let createdSlug = "";
192+
yield* Effect.ensuring(
193+
Effect.gen(function* () {
194+
yield* browser.session(identity, async ({ page, step }) => {
195+
const input = page.locator("#add-health-check-operation");
196+
const options = page.getByRole("option");
197+
198+
await step("Open the Add form and paste the large spec", async () => {
199+
await page.goto("/integrations/add/openapi", { waitUntil: "networkidle" });
200+
await page.getByPlaceholder("https://api.example.com/openapi.json").fill(spec);
201+
await page
202+
.getByRole("heading", { name: "Health check (optional)" })
203+
.waitFor({ timeout: 20_000 });
204+
});
205+
206+
await step("Type to reach an operation past the preview's top slice", async () => {
207+
await input.click();
208+
// Real keystrokes; the operation isn't in the first handful, so it
209+
// is reachable only because the preview now carries the whole spec.
210+
await input.selectText();
211+
await input.pressSequentially(targetSummary, { delay: 10 });
212+
// The real option carries the GET label + toolPath; the freeform
213+
// echo is just the typed text, so "GET" disambiguates them.
214+
const match = options.filter({ hasText: targetSummary }).filter({ hasText: "GET" });
215+
await match.first().waitFor({ timeout: 10_000 });
216+
});
217+
218+
await step("Select the found operation and add the integration", async () => {
219+
const match = options.filter({ hasText: targetSummary }).filter({ hasText: "GET" });
220+
for (let i = 0; i < 8; i++) {
221+
if ((await match.first().getAttribute("data-highlighted")) !== null) break;
222+
await input.press("ArrowDown");
223+
}
224+
await input.press("Enter");
225+
await page.getByRole("button", { name: "Add integration" }).click();
226+
await page.waitForURL(/\/integrations\/[^/?#]+$/, { timeout: 30_000 });
227+
const url = page.url().match(/\/integrations\/([^/?#]+)/);
228+
createdSlug = url?.[1] ?? "";
229+
});
230+
});
231+
232+
expect(createdSlug.length).toBeGreaterThan(0);
233+
const slug = IntegrationSlug.make(createdSlug);
234+
// The drafted check persisted the operation behind the matched summary,
235+
// proving the add-screen search reached past the preview's top slice.
236+
const candidates = yield* client.integrations.healthCheckCandidates({ params: { slug } });
237+
const expected = candidates.find((candidate) => candidate.summary === targetSummary);
238+
if (!expected) return yield* Effect.die("created integration is missing the target op");
239+
const stored = yield* client.integrations.healthCheckGet({ params: { slug } });
240+
expect(stored?.operation).toBe(expected.operation);
241+
}),
242+
Effect.gen(function* () {
243+
if (createdSlug.length > 0) {
244+
yield* client.openapi
245+
.removeSpec({ params: { slug: IntegrationSlug.make(createdSlug) } })
246+
.pipe(Effect.ignore);
247+
}
248+
}),
249+
);
250+
}),
251+
),
252+
);

0 commit comments

Comments
 (0)