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 | }; | ||