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 @@
1import { Effect } from "effect";
2
3import { 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
14export interface GithubResolverOptions {
15 readonly baseUrl: string;
16 /** Bearer token per connection name. */
17 readonly tokens: Readonly<Record<string, string>>;
18}
19
20export 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};