| ... | @@ -2566,7 +2566,145 @@ it.effect("refuses Azure cross-organization reads and writes without its checkou | ... | @@ -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 | Effect.gen(function* () { | 2708 | Effect.gen(function* () { |
| 2571 | const service = yield* makeService({ | 2709 | const service = yield* makeService({ |
| 2572 | projects: [ | 2710 | projects: [ |