History

Filter GitHub source entries safely

Rhys Sullivan committed 4 months ago

Commit 9e77e341 parent bb155804 files · +159 −29Browse files

Showing the changes to packages/plugins/apps/src/source/github-source.test.ts (1 of 4 files). Show all

Unified · Split

packages/plugins/apps/src/source/github-source.test.ts+101−12
@@ -8,6 +8,7 @@ import { Effect } from "effect"; · 7 unmodified lines
88 import { makeSelfHostAppsRuntime } from "../plugin/self-host-runtime";
99 import { makeInMemoryAppsStore, makeTestResolver } from "../testing";
1010 import { PUBLISH_LIMITS } from "../pipeline/publish";
11+import { scopeAddress } from "../seams/scope-address";
1112 import { fetchGitHubSource, syncGitHubSource } from "./github-source";
1213
1314 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
3435 headers: { "content-type": "application/json" },
3536 });
3637
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+
3746 const makeGitHubFetch = (input: {
3847 readonly files: ReadonlyMap<string, string>;
48+ readonly entries?: readonly GitHubTreeFixtureEntry[];
3949 readonly upstreamSha?: string;
4050 readonly treeSha?: string;
4151 }) => {
@@ -43,9 +53,22 @@ const makeGitHubFetch = (input: { · 1 unmodified line
4353 const upstreamSha = input.upstreamSha ?? "commit-1";
4454 const treeSha = input.treeSha ?? "tree-1";
4555 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+ }));
4664 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+ }
4972 }
5073 let blobCalls = 0;
5174 const fetch = (async (rawUrl: string) => {
@@ -59,12 +82,16 @@ const makeGitHubFetch = (input: { · 7 unmodified lines
5982 }
6083 if (url.pathname === `${repoPath}/git/trees/${treeSha}`) {
6184 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+ }),
6895 });
6996 }
7097 const blobPrefix = `${repoPath}/git/blobs/`;
@@ -103,10 +130,10 @@ describe("GitHub custom-tools source", () => { · 32 unmodified lines
103130 ]);
104131 const github = makeGitHubFetch({ files, upstreamSha: "commit-a" });
105132 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" },
110137 ]);
111138 expect(snapshot.description).toBe("Acme tools");
112139
@@ -123,6 +150,7 @@ describe("GitHub custom-tools source", () => { · 10 unmodified lines
123150 expect(result.tools).toEqual(["hello"]);
124151 expect(result.skipped).toEqual([
125152 { path: "workflows/deferred.ts", reason: "not supported yet" },
153+ { path: "docs/readme.md", reason: "ignored" },
126154 ]);
127155 const descriptor = await run(host.runtime.getDescriptor("githubTools"));
128156 expect(descriptor?.description).toBe("Acme tools");
@@ -208,4 +236,65 @@ export default defineTool({ description: "bad", input: { type: "object" }, async · 79 unmodified lines
208236 expect(result.errors?.[0]?.message).toContain('bare import "lodash" is not allowed');
209237 await host.close();
210238 });
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+ });
211300 });