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 | 8 | import { makeSelfHostAppsRuntime } from "../plugin/self-host-runtime"; |
| 9 | 9 | import { makeInMemoryAppsStore, makeTestResolver } from "../testing"; |
| 10 | 10 | import { PUBLISH_LIMITS } from "../pipeline/publish"; |
| 11 | +import { scopeAddress } from "../seams/scope-address"; | |
| 11 | 12 | import { fetchGitHubSource, syncGitHubSource } from "./github-source"; |
| 12 | 13 | |
| 13 | 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 | 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 | 46 | const makeGitHubFetch = (input: { |
| 38 | 47 | readonly files: ReadonlyMap<string, string>; |
| 48 | + readonly entries?: readonly GitHubTreeFixtureEntry[]; | |
| 39 | 49 | readonly upstreamSha?: string; |
| 40 | 50 | readonly treeSha?: string; |
| 41 | 51 | }) => { |
| @@ -43,9 +53,22 @@ const makeGitHubFetch = (input: { · 1 unmodified line | ||
| 43 | 53 | const upstreamSha = input.upstreamSha ?? "commit-1"; |
| 44 | 54 | const treeSha = input.treeSha ?? "tree-1"; |
| 45 | 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 | 64 | let index = 0; |
| 47 | − for (const [path, content] of input.files) { | |
| 48 | − blobBySha.set(`blob-${++index}`, { path, content }); | |
| 65 | + const shaByPath = new Map<string, string>(); | |
| 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 | 73 | let blobCalls = 0; |
| 51 | 74 | const fetch = (async (rawUrl: string) => { |
| @@ -59,12 +82,16 @@ const makeGitHubFetch = (input: { · 7 unmodified lines | ||
| 59 | 82 | } |
| 60 | 83 | if (url.pathname === `${repoPath}/git/trees/${treeSha}`) { |
| 61 | 84 | return json({ |
| 62 | − tree: [...blobBySha.entries()].map(([sha, blob]) => ({ | |
| 63 | − path: blob.path, | |
| 64 | − type: "blob", | |
| 65 | − sha, | |
| 66 | − size: Buffer.byteLength(blob.content, "utf8"), | |
| 67 | − })), | |
| 85 | + tree: treeEntries.map((entry, entryIndex) => { | |
| 86 | + const sha = shaByPath.get(entry.path) ?? `tree-${entryIndex}`; | |
| 87 | + return { | |
| 88 | + path: entry.path, | |
| 89 | + type: entry.type, | |
| 90 | + mode: entry.mode, | |
| 91 | + sha, | |
| 92 | + size: entry.size ?? Buffer.byteLength(entry.content ?? "", "utf8"), | |
| 93 | + }; | |
| 94 | + }), | |
| 68 | 95 | }); |
| 69 | 96 | } |
| 70 | 97 | const blobPrefix = `${repoPath}/git/blobs/`; |
| @@ -103,10 +130,10 @@ describe("GitHub custom-tools source", () => { · 32 unmodified lines | ||
| 103 | 130 | ]); |
| 104 | 131 | const github = makeGitHubFetch({ files, upstreamSha: "commit-a" }); |
| 105 | 132 | const snapshot = await run(fetchGitHubSource({ repo: "acme/tools", fetch: github.fetch })); |
| 106 | − expect([...snapshot.files.keys()].sort()).toEqual([ | |
| 107 | − "executor.json", | |
| 108 | − "tools/hello.ts", | |
| 109 | − "workflows/deferred.ts", | |
| 133 | + expect([...snapshot.files.keys()].sort()).toEqual(["executor.json", "tools/hello.ts"]); | |
| 134 | + expect(snapshot.skipped).toEqual([ | |
| 135 | + { path: "workflows/deferred.ts", reason: "not supported yet" }, | |
| 136 | + { path: "docs/readme.md", reason: "ignored" }, | |
| 110 | 137 | ]); |
| 111 | 138 | expect(snapshot.description).toBe("Acme tools"); |
| 112 | 139 | |
| @@ -123,6 +150,7 @@ describe("GitHub custom-tools source", () => { · 10 unmodified lines | ||
| 123 | 150 | expect(result.tools).toEqual(["hello"]); |
| 124 | 151 | expect(result.skipped).toEqual([ |
| 125 | 152 | { path: "workflows/deferred.ts", reason: "not supported yet" }, |
| 153 | + { path: "docs/readme.md", reason: "ignored" }, | |
| 126 | 154 | ]); |
| 127 | 155 | const descriptor = await run(host.runtime.getDescriptor("githubTools")); |
| 128 | 156 | expect(descriptor?.description).toBe("Acme tools"); |
| @@ -208,4 +236,65 @@ export default defineTool({ description: "bad", input: { type: "object" }, async · 79 unmodified lines | ||
| 208 | 236 | expect(result.errors?.[0]?.message).toContain('bare import "lodash" is not allowed'); |
| 209 | 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 | }); |