Showing the changes to packages/plugins/apps/src/testing/github-resolver.ts (1 of 3 files). Show all
Unified · Split
| @@ -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 | +}; | |