Showing the changes to packages/plugins/apps/src/source/github-source.test.ts (1 of 4 files). Show all
Unified · Split
| @@ -8,6 +8,7 @@ import { Effect } from "effect"; · 7 unmodified lines | |||
| 8 | import { makeSelfHostAppsRuntime } from "../plugin/self-host-runtime"; | 8 | import { makeSelfHostAppsRuntime } from "../plugin/self-host-runtime"; |
| 9 | import { makeInMemoryAppsStore, makeTestResolver } from "../testing"; | 9 | import { makeInMemoryAppsStore, makeTestResolver } from "../testing"; |
| 10 | import { PUBLISH_LIMITS } from "../pipeline/publish"; | 10 | import { PUBLISH_LIMITS } from "../pipeline/publish"; |
| 11 | import { scopeAddress } from "../seams/scope-address"; | ||
| 11 | import { fetchGitHubSource, syncGitHubSource } from "./github-source"; | 12 | import { fetchGitHubSource, syncGitHubSource } from "./github-source"; |
| 12 | 13 | ||
| 13 | const run = <A, E>(effect: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(effect); | 14 | const run = <A, E>(effect: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(effect); |
| @@ -34,8 +35,17 @@ const json = (value: unknown, status = 200): Response => · 20 unmodified lines | |||
| 34 | headers: { "content-type": "application/json" }, | 35 | headers: { "content-type": "application/json" }, |
| 35 | }); | 36 | }); |
| 36 | 37 | ||
| 38 | interface GitHubTreeFixtureEntry { | ||
| 39 | readonly path: string; | ||
| 40 | readonly type: string; | ||
| 41 | readonly mode?: string; | ||
| 42 | readonly content?: string; | ||
| 43 | readonly size?: number; | ||
| 44 | } | ||
| 45 | |||
| 37 | const makeGitHubFetch = (input: { | 46 | const makeGitHubFetch = (input: { |
| 38 | readonly files: ReadonlyMap<string, string>; | 47 | readonly files: ReadonlyMap<string, string>; |
| 48 | readonly entries?: readonly GitHubTreeFixtureEntry[]; | ||
| 39 | readonly upstreamSha?: string; | 49 | readonly upstreamSha?: string; |
| 40 | readonly treeSha?: string; | 50 | readonly treeSha?: string; |
| 41 | }) => { | 51 | }) => { |
| @@ -43,9 +53,22 @@ const makeGitHubFetch = (input: { · 1 unmodified line | |||
| 43 | const upstreamSha = input.upstreamSha ?? "commit-1"; | 53 | const upstreamSha = input.upstreamSha ?? "commit-1"; |
| 44 | const treeSha = input.treeSha ?? "tree-1"; | 54 | const treeSha = input.treeSha ?? "tree-1"; |
| 45 | const blobBySha = new Map<string, { path: string; content: string }>(); | 55 | const blobBySha = new Map<string, { path: string; content: string }>(); |
| 56 | const treeEntries: readonly GitHubTreeFixtureEntry[] = | ||
| 57 | input.entries ?? | ||
| 58 | [...input.files].map(([path, content]) => ({ | ||
| 59 | path, | ||
| 60 | type: "blob", | ||
| 61 | mode: "100644", | ||
| 62 | content, | ||
| 63 | })); | ||
| 46 | let index = 0; | 64 | let index = 0; |
| 47 | for (const [path, content] of input.files) { | 65 | const shaByPath = new Map<string, string>(); |
| 48 | blobBySha.set(`blob-${++index}`, { path, content }); | 66 | for (const entry of treeEntries) { |
| 67 | if (entry.content !== undefined) { | ||
| 68 | const sha = `blob-${++index}`; | ||
| 69 | shaByPath.set(entry.path, sha); | ||
| 70 | blobBySha.set(sha, { path: entry.path, content: entry.content }); | ||
| 71 | } | ||
| 49 | } | 72 | } |
| 50 | let blobCalls = 0; | 73 | let blobCalls = 0; |
| 51 | const fetch = (async (rawUrl: string) => { | 74 | const fetch = (async (rawUrl: string) => { |
| @@ -59,12 +82,16 @@ const makeGitHubFetch = (input: { · 7 unmodified lines | |||
| 59 | } | 82 | } |
| 60 | if (url.pathname === `${repoPath}/git/trees/${treeSha}`) { | 83 | if (url.pathname === `${repoPath}/git/trees/${treeSha}`) { |
| 61 | return json({ | 84 | return json({ |
| 62 | tree: [...blobBySha.entries()].map(([sha, blob]) => ({ | 85 | tree: treeEntries.map((entry, entryIndex) => { |
| 63 | path: blob.path, | 86 | const sha = shaByPath.get(entry.path) ?? `tree-${entryIndex}`; |
| 64 | type: "blob", | 87 | return { |
| 65 | sha, | 88 | path: entry.path, |
| 66 | size: Buffer.byteLength(blob.content, "utf8"), | 89 | type: entry.type, |
| 67 | })), | 90 | mode: entry.mode, |
| 91 | sha, | ||
| 92 | size: entry.size ?? Buffer.byteLength(entry.content ?? "", "utf8"), | ||
| 93 | }; | ||
| 94 | }), | ||
| 68 | }); | 95 | }); |
| 69 | } | 96 | } |
| 70 | const blobPrefix = `${repoPath}/git/blobs/`; | 97 | const blobPrefix = `${repoPath}/git/blobs/`; |
| @@ -103,10 +130,10 @@ describe("GitHub custom-tools source", () => { · 32 unmodified lines | |||
| 103 | ]); | 130 | ]); |
| 104 | const github = makeGitHubFetch({ files, upstreamSha: "commit-a" }); | 131 | const github = makeGitHubFetch({ files, upstreamSha: "commit-a" }); |
| 105 | const snapshot = await run(fetchGitHubSource({ repo: "acme/tools", fetch: github.fetch })); | 132 | const snapshot = await run(fetchGitHubSource({ repo: "acme/tools", fetch: github.fetch })); |
| 106 | expect([...snapshot.files.keys()].sort()).toEqual([ | 133 | expect([...snapshot.files.keys()].sort()).toEqual(["executor.json", "tools/hello.ts"]); |
| 107 | "executor.json", | 134 | expect(snapshot.skipped).toEqual([ |
| 108 | "tools/hello.ts", | 135 | { path: "workflows/deferred.ts", reason: "not supported yet" }, |
| 109 | "workflows/deferred.ts", | 136 | { path: "docs/readme.md", reason: "ignored" }, |
| 110 | ]); | 137 | ]); |
| 111 | expect(snapshot.description).toBe("Acme tools"); | 138 | expect(snapshot.description).toBe("Acme tools"); |
| 112 | 139 | ||
| @@ -123,6 +150,7 @@ describe("GitHub custom-tools source", () => { · 10 unmodified lines | |||
| 123 | expect(result.tools).toEqual(["hello"]); | 150 | expect(result.tools).toEqual(["hello"]); |
| 124 | expect(result.skipped).toEqual([ | 151 | expect(result.skipped).toEqual([ |
| 125 | { path: "workflows/deferred.ts", reason: "not supported yet" }, | 152 | { path: "workflows/deferred.ts", reason: "not supported yet" }, |
| 153 | { path: "docs/readme.md", reason: "ignored" }, | ||
| 126 | ]); | 154 | ]); |
| 127 | const descriptor = await run(host.runtime.getDescriptor("githubTools")); | 155 | const descriptor = await run(host.runtime.getDescriptor("githubTools")); |
| 128 | expect(descriptor?.description).toBe("Acme tools"); | 156 | expect(descriptor?.description).toBe("Acme tools"); |
| @@ -208,4 +236,65 @@ export default defineTool({ description: "bad", input: { type: "object" }, async · 79 unmodified lines | |||
| 208 | expect(result.errors?.[0]?.message).toContain('bare import "lodash" is not allowed'); | 236 | expect(result.errors?.[0]?.message).toContain('bare import "lodash" is not allowed'); |
| 209 | await host.close(); | 237 | await host.close(); |
| 210 | }); | 238 | }); |
| 239 | |||
| 240 | it("skips unsupported tree entries and invalid paths without committing them", async () => { | ||
| 241 | const github = makeGitHubFetch({ | ||
| 242 | files: new Map(), | ||
| 243 | entries: [ | ||
| 244 | { | ||
| 245 | path: "tools/ok.ts", | ||
| 246 | type: "blob", | ||
| 247 | mode: "100644", | ||
| 248 | content: toolSource("ok"), | ||
| 249 | }, | ||
| 250 | { | ||
| 251 | path: "tools/link.ts", | ||
| 252 | type: "blob", | ||
| 253 | mode: "120000", | ||
| 254 | content: "../outside.ts", | ||
| 255 | }, | ||
| 256 | { | ||
| 257 | path: "tools/submodule.ts", | ||
| 258 | type: "commit", | ||
| 259 | mode: "160000", | ||
| 260 | }, | ||
| 261 | { | ||
| 262 | path: "tools/Bad.Name.ts", | ||
| 263 | type: "blob", | ||
| 264 | mode: "100644", | ||
| 265 | content: toolSource("bad"), | ||
| 266 | }, | ||
| 267 | ], | ||
| 268 | upstreamSha: "mixed-sha", | ||
| 269 | }); | ||
| 270 | const host = makeRuntime(); | ||
| 271 | const result = await run( | ||
| 272 | syncGitHubSource({ | ||
| 273 | runtime: host.runtime, | ||
| 274 | scope: "githubTools", | ||
| 275 | repo: "acme/tools", | ||
| 276 | fetch: github.fetch, | ||
| 277 | }), | ||
| 278 | ); | ||
| 279 | |||
| 280 | expect(result.status).toBe("published"); | ||
| 281 | expect(result.tools).toEqual(["ok"]); | ||
| 282 | expect(result.skipped).toEqual([ | ||
| 283 | { path: "tools/link.ts", reason: "unsupported file type" }, | ||
| 284 | { path: "tools/submodule.ts", reason: "unsupported file type" }, | ||
| 285 | { path: "tools/Bad.Name.ts", reason: "ignored" }, | ||
| 286 | ]); | ||
| 287 | |||
| 288 | const descriptor = await run(host.runtime.getDescriptor("githubTools")); | ||
| 289 | expect(descriptor?.tools.map((tool) => tool.name)).toEqual(["ok"]); | ||
| 290 | const scopeStore = await run( | ||
| 291 | host.runtime.deps.artifactStore.forScope(scopeAddress("org", "githubTools")), | ||
| 292 | ); | ||
| 293 | const snapshotPaths = await run(scopeStore.list(descriptor!.snapshotId as never)); | ||
| 294 | expect(snapshotPaths).toContain("tools/ok.ts"); | ||
| 295 | expect(snapshotPaths).not.toContain("tools/link.ts"); | ||
| 296 | expect(snapshotPaths).not.toContain("tools/submodule.ts"); | ||
| 297 | expect(snapshotPaths).not.toContain("tools/Bad.Name.ts"); | ||
| 298 | await host.close(); | ||
| 299 | }); | ||
| 211 | }); | 300 | }); |