History

apps: end-to-end proof over real GitHub emulator (publish MCP, invoke HTTP, workflow, ui, SSE, skills)

Rhys Sullivan committed 4 months ago

Commit efca3dd1 parent 76be9be3 files · +322 −0Browse files

Showing the changes to packages/plugins/apps/src/testing/github-resolver.ts (1 of 3 files). Show all

Unified · Split

packages/plugins/apps/src/testing/github-resolver.ts+81−0
@@ -0,0 +1,81 @@
1+import { Effect } from "effect";
2+
3+import { BindingError, type ClientResolver } from "../plugin/bindings";
4+
5+// ---------------------------------------------------------------------------
6+// A ClientResolver that maps the daily-brief github client methods to real
7+// GitHub REST calls against a base URL (the emulate GitHub emulator in the
8+// e2e). This stands in for the platform invoke path: in production the resolver
9+// routes through the catalog (credentials injected platform-side, policy/audit
10+// applied); here it makes the real authenticated HTTP call with the bound
11+// connection's token. Everything crossing is JSON.
12+// ---------------------------------------------------------------------------
13+
14+export interface GithubResolverOptions {
15+ readonly baseUrl: string;
16+ /** Bearer token per connection name. */
17+ readonly tokens: Readonly<Record<string, string>>;
18+}
19+
20+export const makeGithubRestResolver = (options: GithubResolverOptions): ClientResolver => {
21+ const call = async (
22+ connection: string,
23+ path: readonly string[],
24+ args: readonly unknown[],
25+ ): Promise<unknown> => {
26+ const token = options.tokens[connection];
27+ const headers: Record<string, string> = {
28+ accept: "application/vnd.github+json",
29+ "content-type": "application/json",
30+ };
31+ if (token) headers.authorization = `Bearer ${token}`;
32+ const method = path.join(".");
33+ const input = (args[0] ?? {}) as Record<string, unknown>;
34+
35+ if (method === "repos.listForAuthenticatedUser") {
36+ const perPage = input.per_page ?? 100;
37+ const res = await fetch(`${options.baseUrl}/user/repos?per_page=${perPage}`, { headers });
38+ return (await res.json()) as Array<{ full_name: string }>;
39+ }
40+ if (method === "issues.listForRepo") {
41+ const owner = String(input.owner);
42+ const repo = String(input.repo);
43+ const state = String(input.state ?? "open");
44+ const perPage = input.per_page ?? 100;
45+ const since = input.since ? `&since=${encodeURIComponent(String(input.since))}` : "";
46+ const res = await fetch(
47+ `${options.baseUrl}/repos/${owner}/${repo}/issues?state=${state}&per_page=${perPage}${since}`,
48+ { headers },
49+ );
50+ const issues = (await res.json()) as Array<Record<string, unknown>>;
51+ // Normalize to the shape the tool expects.
52+ return issues.map((i) => ({
53+ number: i.number,
54+ title: i.title,
55+ labels: Array.isArray(i.labels)
56+ ? (i.labels as Array<{ name?: string } | string>).map((l) =>
57+ typeof l === "string" ? { name: l } : { name: l.name ?? "" },
58+ )
59+ : [],
60+ assignee: i.assignee ?? null,
61+ updated_at: i.updated_at,
62+ html_url: i.html_url,
63+ pull_request: i.pull_request,
64+ }));
65+ }
66+ throw new Error(`unsupported github method: ${method}`);
67+ };
68+
69+ return {
70+ call: ({ connection, path, args }) =>
71+ Effect.tryPromise({
72+ try: () => call(connection, path, args),
73+ catch: (cause) =>
74+ new BindingError({
75+ message: cause instanceof Error ? cause.message : String(cause),
76+ role: "github",
77+ surface: "github",
78+ }),
79+ }),
80+ };
81+};