| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | import { |
| 3 | IsoDateTime, |
| 4 | ProviderInstanceId, |
| 5 | type PullRequestReviewGuideResult, |
| 6 | } from "@t3tools/contracts"; |
| 7 | import { guideNoticeState, reviewGuideState } from "./reviewGuideState"; |
| 8 | |
| 9 | const guide: NonNullable<PullRequestReviewGuideResult["guide"]> = { |
| 10 | headSha: "old-head", |
| 11 | generatedAt: IsoDateTime.make("2026-10-11T00:00:00.000Z"), |
| 12 | modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5" }, |
| 13 | stops: [], |
| 14 | }; |
| 15 | |
| 16 | describe("mobile review guide state", () => { |
| 17 | it("keeps a previous guide readable during a rewrite", () => { |
| 18 | const state = reviewGuideState({ |
| 19 | guide, |
| 20 | currentHeadSha: "new-head", |
| 21 | run: { _tag: "Running", headSha: "new-head", startedAt: guide.generatedAt }, |
| 22 | }); |
| 23 | expect(state).toMatchObject({ guide, stale: true, running: true, failure: null }); |
| 24 | }); |
| 25 | it("does not label an unknown head stale", () => { |
| 26 | expect(reviewGuideState({ guide, currentHeadSha: null, run: null }).stale).toBe(false); |
| 27 | }); |
| 28 | it("shows only failures for the current head", () => { |
| 29 | const run = { |
| 30 | _tag: "Failed" as const, |
| 31 | headSha: "old-head", |
| 32 | failedAt: guide.generatedAt, |
| 33 | detail: "The writer disconnected.", |
| 34 | }; |
| 35 | expect(reviewGuideState({ guide: null, currentHeadSha: "old-head", run }).failure).toBe( |
| 36 | run.detail, |
| 37 | ); |
| 38 | expect(reviewGuideState({ guide: null, currentHeadSha: "new-head", run }).failure).toBeNull(); |
| 39 | }); |
| 40 | it("starts empty without claiming a job is running", () => { |
| 41 | expect(reviewGuideState(null)).toEqual({ |
| 42 | guide: null, |
| 43 | stale: false, |
| 44 | running: false, |
| 45 | failure: null, |
| 46 | }); |
| 47 | }); |
| 48 | }); |
| 49 | |
| 50 | const view = { |
| 51 | data: null, |
| 52 | isPending: false, |
| 53 | running: false, |
| 54 | startError: null, |
| 55 | error: null, |
| 56 | }; |
| 57 | |
| 58 | describe("guide notice transitions", () => { |
| 59 | it("does not flash an empty guide before the first response", () => { |
| 60 | expect(guideNoticeState({ ...view, isPending: true }).kind).toBe("loading"); |
| 61 | expect( |
| 62 | guideNoticeState({ ...view, data: { guide: null, run: null, currentHeadSha: "head" } }).kind, |
| 63 | ).toBe("empty"); |
| 64 | }); |
| 65 | |
| 66 | it("keeps cached guides visible while refreshing", () => { |
| 67 | expect( |
| 68 | guideNoticeState({ |
| 69 | ...view, |
| 70 | isPending: true, |
| 71 | data: { guide, run: null, currentHeadSha: guide.headSha }, |
| 72 | }).kind, |
| 73 | ).toBe("ready"); |
| 74 | }); |
| 75 | |
| 76 | it("hides the previous failure as soon as a retry starts", () => { |
| 77 | const data = { |
| 78 | guide: null, |
| 79 | currentHeadSha: "head", |
| 80 | run: { |
| 81 | _tag: "Failed" as const, |
| 82 | headSha: "head", |
| 83 | failedAt: guide.generatedAt, |
| 84 | detail: "Writer disconnected", |
| 85 | }, |
| 86 | }; |
| 87 | expect(guideNoticeState({ ...view, data }).kind).toBe("failed"); |
| 88 | expect(guideNoticeState({ ...view, data, running: true }).kind).toBe("writing"); |
| 89 | }); |
| 90 | |
| 91 | it("keeps a stale guide readable through rewriting and completion", () => { |
| 92 | const data = { guide, currentHeadSha: "head", run: null }; |
| 93 | expect(guideNoticeState({ ...view, data }).kind).toBe("stale"); |
| 94 | expect(guideNoticeState({ ...view, data, running: true }).kind).toBe("writing"); |
| 95 | expect( |
| 96 | guideNoticeState({ ...view, data: { ...data, guide: { ...guide, headSha: "head" } } }).kind, |
| 97 | ).toBe("ready"); |
| 98 | }); |
| 99 | }); |
| 100 | |
| 101 | it("distinguishes a failed read from a failed writer", () => { |
| 102 | expect(guideNoticeState({ ...view, error: "Connection lost" }).kind).toBe("unavailable"); |
| 103 | }); |