| ... | ... | @@ -2566,7 +2566,145 @@ it.effect("refuses Azure cross-organization reads and writes without its checkou |
| 2566 | 2566 | }), |
| 2567 | 2567 | ); |
| 2568 | 2568 | |
| 2569 | | it.effect("refuses a hosted reference when nothing is checked out from that host", () => |
| 2569 | it.effect.each([ |
| 2570 | ["github.com", "github"], |
| 2571 | ["bitbucket.org", "bitbucket"], |
| 2572 | ["codeberg.org", "forgejo"], |
| 2573 | ] as const)("reads linked PR summaries and stacks on %s without a checkout", ([host, kind]) => |
| 2574 | Effect.gen(function* () { |
| 2575 | const seen: Array<{ cwd: string; repository: string; host: string }> = []; |
| 2576 | const service = yield* makeService({ |
| 2577 | projects: [project({ id: "inbox", title: "Inbox", workspaceRoot: "/home/alex" })], |
| 2578 | providers: [ |
| 2579 | fakeProvider(SourceControlProviderKind.make(kind), { |
| 2580 | getChangeRequestSummary: (input) => |
| 2581 | Effect.sync(() => { |
| 2582 | seen.push({ cwd: input.cwd, repository: input.repository, host: input.host }); |
| 2583 | return changeRequest(7, "2026-07-02T00:00:00Z"); |
| 2584 | }), |
| 2585 | getChangeRequestStack: (input) => |
| 2586 | Effect.sync(() => { |
| 2587 | seen.push({ cwd: input.cwd, repository: input.repository, host: input.host }); |
| 2588 | return null; |
| 2589 | }), |
| 2590 | }), |
| 2591 | ], |
| 2592 | }); |
| 2593 | const reference = { |
| 2594 | projectId: "inbox" as ProjectId, |
| 2595 | host, |
| 2596 | repository: "acme/api", |
| 2597 | number: 7, |
| 2598 | }; |
| 2599 | const summary = yield* service.summary(reference, { recoverTransientFailure: false }); |
| 2600 | const stack = yield* service.stack(reference, { includeDetails: false }); |
| 2601 | assert.strictEqual(summary.title, "Change request 7"); |
| 2602 | assert.strictEqual(summary.state, "open"); |
| 2603 | assert.strictEqual(summary.projectId, reference.projectId); |
| 2604 | assert.strictEqual(stack, null); |
| 2605 | assert.deepStrictEqual(seen, [ |
| 2606 | { cwd: "/home/alex", repository: "acme/api", host }, |
| 2607 | { cwd: "/home/alex", repository: "acme/api", host }, |
| 2608 | ]); |
| 2609 | }), |
| 2610 | ); |
| 2611 | |
| 2612 | it.effect("prefers a matching checkout over a non-repository project's directory", () => |
| 2613 | Effect.gen(function* () { |
| 2614 | const seen: string[] = []; |
| 2615 | const service = yield* makeService({ |
| 2616 | projects: [ |
| 2617 | project({ id: "inbox", title: "Inbox", workspaceRoot: "/home/alex" }), |
| 2618 | project({ id: "repo", title: "API", workspaceRoot: "/api", repository: "acme/api" }), |
| 2619 | ], |
| 2620 | providers: [ |
| 2621 | fakeProvider(SourceControlProviderKind.make("github"), { |
| 2622 | getChangeRequestSummary: (input) => |
| 2623 | Effect.sync(() => { |
| 2624 | seen.push(input.cwd); |
| 2625 | return changeRequest(7, "2026-07-02T00:00:00Z"); |
| 2626 | }), |
| 2627 | }), |
| 2628 | ], |
| 2629 | }); |
| 2630 | yield* service.summary( |
| 2631 | { |
| 2632 | projectId: "inbox" as ProjectId, |
| 2633 | host: "github.com", |
| 2634 | repository: "acme/api", |
| 2635 | number: 7, |
| 2636 | }, |
| 2637 | { recoverTransientFailure: false }, |
| 2638 | ); |
| 2639 | assert.deepStrictEqual(seen, ["/api"]); |
| 2640 | }), |
| 2641 | ); |
| 2642 | |
| 2643 | it.effect("requires an explicit host for a non-repository project's linked PR", () => |
| 2644 | Effect.gen(function* () { |
| 2645 | const service = yield* makeService({ |
| 2646 | projects: [project({ id: "inbox", title: "Inbox", workspaceRoot: "/home/alex" })], |
| 2647 | providers: [fakeProvider(SourceControlProviderKind.make("github"))], |
| 2648 | }); |
| 2649 | const error = yield* service |
| 2650 | .summary( |
| 2651 | { |
| 2652 | projectId: "inbox" as ProjectId, |
| 2653 | repository: "acme/api", |
| 2654 | number: 7, |
| 2655 | }, |
| 2656 | { recoverTransientFailure: false }, |
| 2657 | ) |
| 2658 | .pipe(Effect.flip); |
| 2659 | assert.strictEqual(error._tag, "PullRequestUnavailableError"); |
| 2660 | }), |
| 2661 | ); |
| 2662 | |
| 2663 | it.effect("refuses a public host reference without a selected project directory", () => |
| 2664 | Effect.gen(function* () { |
| 2665 | const service = yield* makeService({ |
| 2666 | projects: [], |
| 2667 | providers: [fakeProvider(SourceControlProviderKind.make("github"))], |
| 2668 | }); |
| 2669 | const error = yield* service |
| 2670 | .summary( |
| 2671 | { |
| 2672 | projectId: "missing" as ProjectId, |
| 2673 | host: "github.com", |
| 2674 | repository: "acme/api", |
| 2675 | number: 7, |
| 2676 | }, |
| 2677 | { recoverTransientFailure: false }, |
| 2678 | ) |
| 2679 | .pipe(Effect.flip); |
| 2680 | assert.strictEqual(error._tag, "PullRequestUnavailableError"); |
| 2681 | }), |
| 2682 | ); |
| 2683 | |
| 2684 | it.effect( |
| 2685 | "requires a checkout for GitLab because its adapter does not explicitly target the host", |
| 2686 | () => |
| 2687 | Effect.gen(function* () { |
| 2688 | const service = yield* makeService({ |
| 2689 | projects: [project({ id: "inbox", title: "Inbox", workspaceRoot: "/home/alex" })], |
| 2690 | providers: [fakeProvider(SourceControlProviderKind.make("gitlab"))], |
| 2691 | }); |
| 2692 | const error = yield* service |
| 2693 | .summary( |
| 2694 | { |
| 2695 | projectId: "inbox" as ProjectId, |
| 2696 | host: "gitlab.com", |
| 2697 | repository: "acme/api", |
| 2698 | number: 7, |
| 2699 | }, |
| 2700 | { recoverTransientFailure: false }, |
| 2701 | ) |
| 2702 | .pipe(Effect.flip); |
| 2703 | assert.strictEqual(error._tag, "PullRequestUnavailableError"); |
| 2704 | }), |
| 2705 | ); |
| 2706 | |
| 2707 | it.effect("refuses a hosted reference when its provider is unavailable", () => |
| 2570 | 2708 | Effect.gen(function* () { |
| 2571 | 2709 | const service = yield* makeService({ |
| 2572 | 2710 | projects: [ |