Skip to content
Source
Sign inSign up
Open History to see the last commit

WikiPage.test.ts

678 lines25.7 KB

1import * as GitHub from "@alchemy.run/github";
2import { fromToken } from "@alchemy.run/github/Credentials";
3import type { WikiPage, WikiPageProps } from "@alchemy.run/github/WikiPage";
4import { WikiPageProvider } from "@alchemy.run/github/WikiPage";
5import {
6 deleteWikiPage,
7 readWikiPage,
8 syncWikiPage,
9 wikiRepository,
10 type WikiRepository,
11} from "@alchemy.run/github/WikiPage";
12import { layer } from "@alchemy.run/test";
13import { expect } from "@alchemy.run/test";
14import * as Test from "@alchemy.run/test/Alchemy";
15import * as NodeServices from "@effect/platform-node/NodeServices";
16import * as Output from "alchemy/Output";
17import * as Provider from "alchemy/Provider";
18import { exec } from "alchemy/Util/exec";
19import * as Effect from "effect/Effect";
20import * as FileSystem from "effect/FileSystem";
21import * as Path from "effect/Path";
22import * as ChildProcess from "effect/process/ChildProcess";
23import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
24import * as Redacted from "effect/Redacted";
25import * as Result from "effect/Result";
26import * as Stream from "effect/Stream";
27​
28const owner = process.env.GITHUB_TEST_OWNER ?? "alchemy-run-test";
29if (!["alchemy-run-test", "alchemy-run-test-2"].includes(owner)) {
30 throw new Error("GITHUB_TEST_OWNER must be alchemy-run-test or alchemy-run-test-2");
32​
33const { test } = Test.make({ providers: GitHub.providers({ baseUrl: "github.com" }) });
34​
35// Repositories are retained because the testing token lacks delete_repo.
36const fixture = (suffix: string) =>
37 GitHub.Repository("Repo", {
38 owner,
39 name: `alchemy-pr-1578-wiki-${suffix}`,
40 visibility: "public",
41 hasWiki: true,
42 autoInit: true,
43 });
44​
45const repoNameOf = (repo: GitHub.Repository) =>
46 Output.map(repo.fullName, (fullName) => fullName.split("/")[1]!);
47​
48const git = Effect.fn(
49 function* (cwd: string, ...args: string[]) {
50 const handle = yield* ChildProcess.make("git", args, {
51 cwd,
52 env: { GIT_TERMINAL_PROMPT: "0" },
53 extendEnv: true,
54 });
55 const [exitCode, stdout, stderr] = yield* Effect.all(
56 [
57 handle.exitCode,
58 Stream.mkString(Stream.decodeText(handle.stdout)),
59 Stream.mkString(Stream.decodeText(handle.stderr)),
60 ],
61 { concurrency: 3 },
62 );
63 if (exitCode !== 0) {
64 return yield* Effect.fail(new Error(`git ${args[0]} exited ${exitCode}: ${stderr}`));
65 }
66 return stdout;
67 },
68 Effect.scoped,
69 Effect.timeout("30 seconds"),
70);
71​
72// Read the public wiki's Git repository independently of the resource provider.
73const getPage = (repository: string, title: string, extension = "md") =>
74 Effect.gen(function* () {
75 const fs = yield* FileSystem.FileSystem;
76 const path = yield* Path.Path;
77 const cwd = yield* Effect.sync(() => process.cwd());
78 const directory = yield* fs.makeTempDirectoryScoped({ directory: cwd, prefix: ".wiki-test-" });
79 yield* git(
80 directory,
81 "clone",
82 "--quiet",
83 `https://github.com/${owner}/${repository}.wiki.git`,
84 "wiki",
85 );
86 const wiki = path.join(directory, "wiki");
87 const file = `${title.replace(/\s+/g, "-")}.${extension}`;
88 if (!(yield* fs.exists(path.join(wiki, file)))) return undefined;
89 return {
90 content: yield* fs.readFileString(path.join(wiki, file)),
91 sha: (yield* git(wiki, "log", "-1", "--format=%H", "--", file)).trim(),
92 };
93 }).pipe(Effect.scoped);
94​
95// TODO: GitHub requires the first wiki page to be saved in its web UI.
96// Repository hasWiki/autoInit cannot bootstrap the separate wiki Git repository.
97test.provider.todo(
98 "create and update wiki page",
99 (stack) =>
100 Effect.gen(function* () {
101 yield* stack.destroy();
102 const deploy = (content: string) =>
103 stack.deploy(
104 Effect.gen(function* () {
105 const repo = yield* fixture("lifecycle");
106 const page = yield* GitHub.WikiPage("Page", {
107 owner,
108 repository: repoNameOf(repo),
109 title: "Home",
110 content,
111 allowDelete: true,
112 });
113 return { repo, page };
114 }),
115 );
116​
117 const created = yield* deploy("Welcome to the wiki!");
118 expect(created.page.title).toBe("Home");
119 expect(created.page.pageName).toBe("Home");
120 expect(created.page.htmlUrl).toBe(
121 `https://github.com/${owner}/alchemy-pr-1578-wiki-lifecycle/wiki/Home`,
122 );
123 expect((yield* getPage("alchemy-pr-1578-wiki-lifecycle", "Home"))?.content).toBe(
124 "Welcome to the wiki!",
125 );
126​
127 const updated = yield* deploy("# Updated Content\n\nThis is updated!");
128 expect(updated.page.sha).not.toBe(created.page.sha);
129 expect((yield* getPage("alchemy-pr-1578-wiki-lifecycle", "Home"))?.content).toBe(
130 "# Updated Content\n\nThis is updated!",
131 );
132​
133 yield* stack.destroy();
134 expect(yield* getPage("alchemy-pr-1578-wiki-lifecycle", "Home")).toBeUndefined();
135 }),
136 {
137 tags: ["provider:github", "provider:github:repository", "provider:github:wikipage", "live"],
138 timeout: 120_000,
139 },
140);
141​
142test.provider.todo(
143 "create page with custom format",
144 (stack) =>
145 Effect.gen(function* () {
146 yield* stack.destroy();
147 const result = yield* stack.deploy(
148 Effect.gen(function* () {
149 const repo = yield* fixture("format");
150 return yield* GitHub.WikiPage("Page", {
151 owner,
152 repository: repoNameOf(repo),
153 title: "API Documentation",
154 content: `
155 = API Documentation
156​
157 == Overview
158​
159 The API provides...
160 `,
161 format: "asciidoc",
162 message: "Add API documentation",
163 allowDelete: true,
164 });
165 }),
166 );
167 expect(result.title).toBe("API Documentation");
168 expect(result.pageName).toBe("API-Documentation");
169 expect(
170 (yield* getPage("alchemy-pr-1578-wiki-format", "API Documentation", "asciidoc"))?.content,
171 ).toBe("= API Documentation\n\n== Overview\n\nThe API provides...");
172 yield* stack.destroy();
173 expect(
174 yield* getPage("alchemy-pr-1578-wiki-format", "API Documentation", "asciidoc"),
175 ).toBeUndefined();
176 }),
177 {
178 tags: ["provider:github", "provider:github:repository", "provider:github:wikipage", "live"],
179 timeout: 120_000,
180 },
181);
182​
183test.provider.todo(
184 "preserve page when allowDelete is false (default)",
185 (stack) =>
186 Effect.gen(function* () {
187 yield* stack.destroy();
188 const deploy = (allowDelete?: boolean) =>
189 stack.deploy(
190 Effect.gen(function* () {
191 const repo = yield* fixture("preserve");
192 return yield* GitHub.WikiPage("Page", {
193 owner,
194 repository: repoNameOf(repo),
195 title: "Documentation",
196 content: "Important documentation that should be preserved",
197 allowDelete,
198 });
199 }),
200 );
201 const created = yield* deploy();
202 yield* stack.destroy();
203 const retained = yield* getPage("alchemy-pr-1578-wiki-preserve", "Documentation");
204 expect(retained?.sha).toBe(created.sha);
205 expect(retained?.content).toBe("Important documentation that should be preserved");
206​
207 // Re-manage the retained page with deletion enabled to verify cleanup.
208 yield* deploy(true);
209 yield* stack.destroy();
210 expect(yield* getPage("alchemy-pr-1578-wiki-preserve", "Documentation")).toBeUndefined();
211 }),
212 {
213 tags: ["provider:github", "provider:github:repository", "provider:github:wikipage", "live"],
214 timeout: 120_000,
215 },
216);
217​
218test.provider.todo(
219 "replace page when title changes",
220 (stack) =>
221 Effect.gen(function* () {
222 yield* stack.destroy();
223 const deploy = (title: string, content: string) =>
224 stack.deploy(
225 Effect.gen(function* () {
226 const repo = yield* fixture("replace");
227 return yield* GitHub.WikiPage("Page", {
228 owner,
229 repository: repoNameOf(repo),
230 title,
231 content,
232 allowDelete: true,
233 });
234 }),
235 );
236 const created = yield* deploy("Getting Started", "Original guide");
237 expect(created.title).toBe("Getting Started");
238 const replaced = yield* deploy("Quick Start", "New guide with different title");
239 expect(replaced.title).toBe("Quick Start");
240 expect(replaced.pageName).toBe("Quick-Start");
241 expect(yield* getPage("alchemy-pr-1578-wiki-replace", "Getting Started")).toBeUndefined();
242 expect((yield* getPage("alchemy-pr-1578-wiki-replace", "Quick Start"))?.content).toBe(
243 "New guide with different title",
244 );
245 yield* stack.destroy();
246 expect(yield* getPage("alchemy-pr-1578-wiki-replace", "Quick Start")).toBeUndefined();
247 }),
248 {
249 tags: ["provider:github", "provider:github:repository", "provider:github:wikipage", "live"],
250 timeout: 120_000,
251 },
252);
253​
254// Deterministic Git-transport tests over local bare-repository fixtures.
255const props: WikiPageProps = {
256 owner: "alchemy-run-test",
257 repository: "alchemy-pr-1578-wiki-unit",
258 title: "Getting Started",
259 content: "First revision",
260 allowDelete: true,
261};
262const token = "wiki-fixture-secret";
263​
264const fixtureGit = Effect.fn(
265 function* (cwd: string, ...args: string[]) {
266 const env = yield* Effect.sync(() => ({
267 PATH: process.env.PATH,
268 HOME: cwd,
269 GIT_CONFIG_NOSYSTEM: "1",
270 GIT_CONFIG_GLOBAL: "/dev/null",
271 GIT_TERMINAL_PROMPT: "0",
272 GIT_AUTHOR_NAME: "Wiki fixture",
273 GIT_AUTHOR_EMAIL: "wiki-fixture@example.com",
274 GIT_COMMITTER_NAME: "Wiki fixture",
275 GIT_COMMITTER_EMAIL: "wiki-fixture@example.com",
276 }));
277 const result = yield* exec(ChildProcess.make("git", args, { cwd, env, extendEnv: false }));
278 expect(result.exitCode).toBe(0);
279 return result.stdout;
280 },
281 Effect.scoped,
282 Effect.timeout("20 seconds"),
283);
284​
285const gitFixture = Effect.gen(function* () {
286 const fs = yield* FileSystem.FileSystem;
287 const path = yield* Path.Path;
288 const directory = yield* fs.makeTempDirectoryScoped({ prefix: "alchemy-wiki-fixture-" });
289 const remote = path.join(directory, "fixture.wiki.git");
290 const seed = path.join(directory, "seed");
291 yield* fixtureGit(directory, "init", "--bare", "--initial-branch=main", remote);
292 yield* fixtureGit(directory, "clone", remote, seed);
293 yield* fs.writeFileString(path.join(seed, "Bootstrap.md"), "Unmanaged bootstrap page");
294 yield* fixtureGit(seed, "add", "Bootstrap.md");
295 yield* fixtureGit(seed, "commit", "-m", "Bootstrap wiki");
296 yield* fixtureGit(seed, "push", "origin", "HEAD");
297 const repository: WikiRepository = {
298 remote,
299 htmlUrl: `https://github.com/${props.owner}/${props.repository}/wiki`,
300 token: Redacted.make(token),
301 };
302 return { fs, path, directory, seed, repository };
303});
304​
305const describe = layer(NodeServices.layer, { excludeTestServices: true });
306​
307describe("WikiPage Git fixtures", (it) => {
308 it.effect(
309 "runs provider create, recovery, adoption, update, replacement, and delete against Git",
310 () =>
311 Effect.gen(function* () {
312 const { repository } = yield* gitFixture;
313 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
314 const remote = `https://github.com/${props.owner}/${props.repository}.wiki.git`;
315 const localTransport = ChildProcessSpawner.make((command) =>
316 spawner.spawn(
317 command._tag === "StandardCommand"
318 ? ChildProcess.make(
319 command.command,
320 command.args.map((arg) => (arg === remote ? repository.remote : arg)),
321 command.options,
322 )
323 : command,
324 ),
325 );
326 yield* Effect.gen(function* () {
327 const provider = yield* Provider.Provider<WikiPage>("GitHub.WikiPage");
328 const context = {
329 id: "Page",
330 fqn: "Page",
331 instanceId: "fixture",
332 bindings: [],
333 session: { note: () => Effect.void },
334 };
335 const created = yield* provider.reconcile({
336 ...context,
337 news: props,
338 olds: undefined,
339 output: undefined,
340 });
341 expect(yield* provider.read!({ ...context, olds: props, output: undefined })).toEqual(
342 created,
343 );
344 const adopted = yield* provider.reconcile({
345 ...context,
346 news: props,
347 olds: undefined,
348 output: created,
349 });
350 expect(adopted).toEqual(created);
351 const news = { ...props, content: "Changed via provider" };
352 const updated = yield* provider.reconcile({
353 ...context,
354 news,
355 olds: props,
356 output: adopted,
357 });
358 expect(updated.sha).not.toBe(created.sha);
359 yield* provider.delete({
360 ...context,
361 olds: { ...news, allowDelete: undefined },
362 output: updated,
363 });
364 expect(yield* provider.read!({ ...context, olds: news, output: undefined })).toEqual(
365 updated,
366 );
367 const renamed = { ...news, title: "Quick Start" };
368 const replacement = yield* provider.reconcile({
369 ...context,
370 news: renamed,
371 olds: undefined,
372 output: undefined,
373 });
374 yield* provider.delete({ ...context, olds: news, output: updated });
375 expect(
376 yield* provider.read!({ ...context, olds: news, output: undefined }),
377 ).toBeUndefined();
378 expect(yield* provider.read!({ ...context, olds: renamed, output: undefined })).toEqual(
379 replacement,
380 );
381 yield* provider.delete({ ...context, olds: renamed, output: replacement });
382 yield* provider.delete({ ...context, olds: renamed, output: replacement });
383 }).pipe(
384 Effect.provide(WikiPageProvider()),
385 Effect.provide(fromToken(token)),
386 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, localTransport),
387 );
388 }).pipe(Effect.scoped),
389 { tags: ["provider:github", "provider:github:wikipage", "local"] },
390 );
391 it.effect(
392 "creates, recovers, updates, avoids no-op commits, and deletes idempotently",
393 () =>
394 Effect.gen(function* () {
395 const { directory, repository } = yield* gitFixture;
396 expect(yield* readWikiPage(repository, props)).toBeUndefined();
397 const created = yield* syncWikiPage(repository, props);
398 expect(created.pageName).toBe("Getting-Started");
399 expect(created.htmlUrl).toBe(`${repository.htmlUrl}/Getting-Started`);
400 expect(yield* readWikiPage(repository, props)).toEqual(created);
401 expect(yield* syncWikiPage(repository, props)).toEqual(created);
402 const updated = yield* syncWikiPage(repository, {
403 ...props,
404 content: "\n Updated\n indented\n",
405 message: "Update fixture page",
406 });
407 expect(updated.sha).not.toBe(created.sha);
408 expect(
409 (yield* fixtureGit(
410 directory,
411 "--git-dir",
412 repository.remote,
413 "log",
414 "-1",
415 "--format=%s",
416 )).trim(),
417 ).toBe("Update fixture page");
418 expect(
419 yield* fixtureGit(
420 directory,
421 "--git-dir",
422 repository.remote,
423 "show",
424 "HEAD:Getting-Started.md",
425 ),
426 ).toBe("Updated\n indented");
427 yield* deleteWikiPage(repository, props);
428 yield* deleteWikiPage(repository, props);
429 expect(yield* readWikiPage(repository, props)).toBeUndefined();
430 expect(
431 yield* fixtureGit(directory, "--git-dir", repository.remote, "show", "HEAD:Bootstrap.md"),
432 ).toBe("Unmanaged bootstrap page");
433 expect(
434 (yield* fixtureGit(
435 directory,
436 "--git-dir",
437 repository.remote,
438 "rev-list",
439 "--count",
440 "HEAD",
441 )).trim(),
442 ).toBe("4");
443 }).pipe(Effect.scoped),
444 { tags: ["provider:github", "provider:github:wikipage", "local"] },
445 );
446​
447 it.effect(
448 "preserves by default and recreates an externally deleted page",
449 () =>
450 Effect.gen(function* () {
451 const { repository } = yield* gitFixture;
452 const created = yield* syncWikiPage(repository, props);
453 yield* deleteWikiPage(repository, { ...props, allowDelete: undefined });
454 expect(yield* readWikiPage(repository, props)).toEqual(created);
455 yield* deleteWikiPage(repository, props);
456 const recreated = yield* syncWikiPage(repository, props);
457 expect(recreated.sha).not.toBe(created.sha);
458 expect(yield* readWikiPage(repository, props)).toEqual(recreated);
459 yield* deleteWikiPage(repository, props);
460 }).pipe(Effect.scoped),
461 { tags: ["provider:github", "provider:github:wikipage", "local"] },
462 );
463​
464 it.effect(
465 "converges all formats, removes previous extensions, and preserves unrelated pages",
466 () =>
467 Effect.gen(function* () {
468 const { directory, repository } = yield* gitFixture;
469 const formats = {
470 markdown: "md",
471 asciidoc: "asciidoc",
472 mediawiki: "mediawiki",
473 org: "org",
474 pod: "pod",
475 rdoc: "rdoc",
476 rest: "rst",
477 textile: "textile",
478 } as const;
479 for (const format of Object.keys(formats) as Array<keyof typeof formats>) {
480 const next = { ...props, format };
481 const page = yield* syncWikiPage(repository, next);
482 expect(yield* readWikiPage(repository, props)).toEqual(page);
483 expect(
484 (yield* fixtureGit(
485 directory,
486 "--git-dir",
487 repository.remote,
488 "ls-tree",
489 "--name-only",
490 "HEAD",
491 ))
492 .trim()
493 .split("\n"),
494 ).toEqual(["Bootstrap.md", `Getting-Started.${formats[format]}`]);
495 }
496 yield* deleteWikiPage(repository, props);
497 expect(yield* readWikiPage(repository, props)).toBeUndefined();
498 }).pipe(Effect.scoped),
499 { tags: ["provider:github", "provider:github:wikipage", "local"] },
500 );
501​
502 it.effect(
503 "recovers alternate extensions and safely replaces symlink pages",
504 () =>
505 Effect.gen(function* () {
506 const { fs, path, directory, seed, repository } = yield* gitFixture;
507 yield* fs.writeFileString(path.join(seed, "Getting-Started.markdown"), "Existing page");
508 const outside = path.join(directory, "outside");
509 yield* fs.writeFileString(outside, "Do not overwrite");
510 yield* fs.symlink(outside, path.join(seed, "Linked.md"));
511 yield* fixtureGit(seed, "add", ".");
512 yield* fixtureGit(seed, "commit", "-m", "External pages");
513 yield* fixtureGit(seed, "push", "origin", "HEAD");
514 expect(yield* readWikiPage(repository, props)).toBeDefined();
515 yield* syncWikiPage(repository, props);
516 yield* syncWikiPage(repository, { ...props, title: "Linked" });
517 expect(yield* fs.readFileString(outside)).toBe("Do not overwrite");
518 expect(
519 (yield* fixtureGit(
520 directory,
521 "--git-dir",
522 repository.remote,
523 "ls-tree",
524 "HEAD",
525 "Linked.md",
526 )).startsWith("100644"),
527 ).toBe(true);
528 expect(
529 (yield* fixtureGit(
530 directory,
531 "--git-dir",
532 repository.remote,
533 "ls-tree",
534 "--name-only",
535 "HEAD",
536 )).includes("Getting-Started.markdown"),
537 ).toBe(false);
538 }).pipe(Effect.scoped),
539 { tags: ["provider:github", "provider:github:wikipage", "local"] },
540 );
541​
542 it.effect(
543 "retries concurrent pushes without losing sibling pages",
544 () =>
545 Effect.gen(function* () {
546 const { repository } = yield* gitFixture;
547 const titles = ["First", "Second", "Third"];
548 yield* Effect.all(
549 titles.map((title) => syncWikiPage(repository, { ...props, title })),
550 { concurrency: 3 },
551 );
552 for (const title of titles)
553 expect(yield* readWikiPage(repository, { ...props, title })).toBeDefined();
554 }).pipe(Effect.scoped),
555 { tags: ["provider:github", "provider:github:wikipage", "local"] },
556 );
557​
558 it.effect(
559 "reports unavailable repositories with actionable errors without skipping lifecycle work",
560 () =>
561 Effect.gen(function* () {
562 const { directory, path, repository } = yield* gitFixture;
563 const missing = { ...repository, remote: path.join(directory, "uninitialized.wiki.git") };
564 const result = yield* Effect.result(syncWikiPage(missing, props));
565 expect(Result.isFailure(result)).toBe(true);
566 if (Result.isFailure(result)) {
567 expect(result.failure._tag).toBe("WikiRepositoryUnavailable");
568 expect(result.failure.message).toContain("first page in the GitHub web UI");
569 expect(result.failure.message).not.toContain(token);
570 }
571 expect(yield* readWikiPage(missing, props)).toBeUndefined();
572 yield* deleteWikiPage(missing, props);
573 }).pipe(Effect.scoped),
574 { tags: ["provider:github", "provider:github:wikipage", "local"] },
575 );
576​
577 it.effect(
578 "keeps credentials out of arguments and config and removes temporary checkouts",
579 () =>
580 Effect.gen(function* () {
581 const { fs, repository } = yield* gitFixture;
582 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
583 const directories = new Set<string>();
584 const safeSpawner = ChildProcessSpawner.make((command) =>
585 Effect.gen(function* () {
586 if (command._tag === "StandardCommand") {
587 expect(command.args.join(" ")).not.toContain(token);
588 expect(command.options.extendEnv).toBe(false);
589 const env = command.options.env!;
590 expect(env.GIT_CONFIG_KEY_0).toBe(`http.${repository.remote}.extraHeader`);
591 expect(env.GIT_CONFIG_VALUE_0).toMatch(/^Authorization: Basic /);
592 expect(env.GIT_TRACE).toBeUndefined();
593 const plain = (value: string | Redacted.Redacted<string> | undefined) =>
594 Redacted.isRedacted(value) ? Redacted.value(value) : value!;
595 directories.add(plain(env.HOME));
596 expect(yield* fs.readFileString(plain(env.GIT_CONFIG_GLOBAL))).toBe("");
597 }
598 return yield* spawner.spawn(command);
599 }),
600 );
601 yield* syncWikiPage(repository, props).pipe(
602 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, safeSpawner),
603 );
604 expect(directories.size).toBe(1);
605 for (const directory of directories) expect(yield* fs.exists(directory)).toBe(false);
606 }).pipe(Effect.scoped),
607 { tags: ["provider:github", "provider:github:wikipage", "local"] },
608 );
609​
610 it.effect(
611 "rejects unsafe titles and produces encoded browser URLs",
612 () =>
613 Effect.gen(function* () {
614 const { repository } = yield* gitFixture;
615 for (const title of ["../escape", "a/b", "a\\b", "", "..", " line", "line\nbreak"]) {
616 const result = yield* Effect.result(syncWikiPage(repository, { ...props, title }));
617 expect(Result.isFailure(result)).toBe(true);
618 if (Result.isFailure(result)) expect(result.failure._tag).toBe("InvalidWikiPage");
619 }
620 const page = yield* syncWikiPage(repository, { ...props, title: "API #1?" });
621 expect(page.htmlUrl).toBe(`${repository.htmlUrl}/API-%231%3F`);
622 }).pipe(Effect.scoped),
623 { tags: ["unit", "provider:github", "provider:github:wikipage", "local"] },
624 );
625​
626 it.effect(
627 "maps public, enterprise, and data-residency API hosts to Git hosts",
628 () =>
629 Effect.gen(function* () {
630 const cases = [
631 [undefined, "https://enterprise.example.com"],
632 ["github.com", "https://github.com"],
633 ["https://wiki.example.com:8443/api/v3", "https://wiki.example.com:8443"],
634 ["https://api.acme.ghe.com", "https://acme.ghe.com"],
635 ] as const;
636 for (const [baseUrl, origin] of cases) {
637 const repository = yield* wikiRepository({ ...props, baseUrl });
638 expect(repository.remote).toBe(`${origin}/${props.owner}/${props.repository}.wiki.git`);
639 expect(repository.htmlUrl).toBe(`${origin}/${props.owner}/${props.repository}/wiki`);
640 }
641 const insecure = yield* Effect.result(
642 wikiRepository({ ...props, baseUrl: "http://wiki.example.com" }),
643 );
644 expect(Result.isFailure(insecure)).toBe(true);
645 }).pipe(Effect.provide(fromToken(token, { baseUrl: "enterprise.example.com" }))),
646 { tags: ["unit", "provider:github", "provider:github:wikipage", "local"] },
647 );
648​
649 it.effect(
650 "replaces title, repository, owner, and host changes but updates formats in place",
651 () =>
652 Effect.gen(function* () {
653 const provider = yield* Provider.Provider<WikiPage>("GitHub.WikiPage");
654 const diff = (news: WikiPageProps) =>
655 provider.diff!({
656 id: "Page",
657 fqn: "Page",
658 instanceId: "fixture",
659 olds: props,
660 news,
661 oldBindings: [],
662 newBindings: [],
663 output: undefined,
664 });
665 for (const news of [
666 { ...props, title: "Renamed" },
667 { ...props, repository: "other" },
668 { ...props, owner: "alchemy-run-test-2" },
669 { ...props, baseUrl: "enterprise.example.com" },
670 ])
671 expect(yield* diff(news)).toEqual({ action: "replace" });
672 expect(yield* diff({ ...props, format: "asciidoc" })).toBeUndefined();
673 expect(yield* diff({ ...props, title: "Getting-Started" })).toBeUndefined();
674 expect(yield* diff({ ...props, baseUrl: "https://api.github.com" })).toBeUndefined();
675 }).pipe(Effect.provide(WikiPageProvider()), Effect.provide(fromToken(token))),
676 { tags: ["unit", "provider:github", "provider:github:wikipage", "local"] },
677 );
678});