| 1 | /** |
| 2 | * Pins `./gitCafePullRequestJson.ts`, pure decoding of GitCafe's REST JSON into the neutral |
| 3 | * `Provider*` shapes. Pass 2 creates these exports: |
| 4 | * |
| 5 | * Schemas (REST bodies as GitCafe sends them): |
| 6 | * - `GitCafePull` (list row), `GitCafePullDetail` (row + description, closedAt, mergedAt, |
| 7 | * sourceRepo, lockedAt?, required `capabilities: { comment, review, merge, edit, moderate }`), |
| 8 | * - `GitCafeComments`, `GitCafeReviews`, `GitCafeCommits`, `GitCafeReactions` |
| 9 | * (cursor envelopes `{ items, next }`; commits also `{ truncated, headOid }`), |
| 10 | * - `GitCafeStackEnvelope` (`{ stack: null | { id, number, revision, landingBase, members } }`, |
| 11 | * members with an optional `headOid`), `GitCafeDiff` (`{ items, truncated? }`), |
| 12 | * `GitCafeChecks` (`{ items, next }`). |
| 13 | * |
| 14 | * Conversions (all take the repository host, so staging stays on staging): |
| 15 | * - `toChangeRequest(pull, { repository, host }): ProviderChangeRequest` |
| 16 | * - `toViewerPermissions(detail): PullRequestViewerPermissions` — fills `editChangeRequest`. |
| 17 | * - `toActivity({ comments, reviews, commits, reactions?, headOid?, host }): |
| 18 | * ProviderChangeRequestActivity` — fills `canEdit` on both copies of a remark and `canResolve` |
| 19 | * on review threads, from each comment's `capabilities`. |
| 20 | * - `toStack(stack, { repository, host }): ProviderChangeRequestStack` |
| 21 | * - `toDiff(diff): ProviderDiffSlice` |
| 22 | * - `toChecks(checks): ReadonlyArray<PullRequestCheck>` |
| 23 | */ |
| 24 | import { assert, describe, it } from "@effect/vitest"; |
| 25 | import * as Schema from "effect/Schema"; |
| 26 | |
| 27 | import * as GitCafePullRequestJson from "./gitCafePullRequestJson.ts"; |
| 28 | |
| 29 | const decodePull = Schema.decodeUnknownSync(GitCafePullRequestJson.GitCafePull); |
| 30 | const decodePullDetail = Schema.decodeUnknownSync(GitCafePullRequestJson.GitCafePullDetail); |
| 31 | const decodeComments = Schema.decodeUnknownSync(GitCafePullRequestJson.GitCafeComments); |
| 32 | const decodeReviews = Schema.decodeUnknownSync(GitCafePullRequestJson.GitCafeReviews); |
| 33 | const decodeCommits = Schema.decodeUnknownSync(GitCafePullRequestJson.GitCafeCommits); |
| 34 | const decodeReactions = Schema.decodeUnknownSync(GitCafePullRequestJson.GitCafeReactions); |
| 35 | const decodeStackEnvelope = Schema.decodeUnknownSync(GitCafePullRequestJson.GitCafeStackEnvelope); |
| 36 | const decodeDiff = Schema.decodeUnknownSync(GitCafePullRequestJson.GitCafeDiff); |
| 37 | const decodeChecks = Schema.decodeUnknownSync(GitCafePullRequestJson.GitCafeChecks); |
| 38 | |
| 39 | const timestamp = "2026-09-12T12:00:00Z"; |
| 40 | const actor = { |
| 41 | kind: "local", |
| 42 | actorId: "act_one", |
| 43 | handle: "alice", |
| 44 | displayName: null, |
| 45 | avatarUrl: "/avatars/one", |
| 46 | }; |
| 47 | const pull = { |
| 48 | id: "pr_one", |
| 49 | number: 7, |
| 50 | title: "Change", |
| 51 | state: "open", |
| 52 | draft: true, |
| 53 | sourceBranch: "feature", |
| 54 | targetBranch: "main", |
| 55 | headOid: "abcdef", |
| 56 | author: actor, |
| 57 | createdAt: timestamp, |
| 58 | updatedAt: timestamp, |
| 59 | version: 1, |
| 60 | }; |
| 61 | const detail = { |
| 62 | ...pull, |
| 63 | draft: false, |
| 64 | description: "Body", |
| 65 | closedAt: null, |
| 66 | mergedAt: null, |
| 67 | sourceRepo: null, |
| 68 | capabilities: { comment: true, review: true, merge: true, edit: true, moderate: true }, |
| 69 | }; |
| 70 | const lineComment = { |
| 71 | author: actor, |
| 72 | body: "Review", |
| 73 | path: "file.ts", |
| 74 | line: 3, |
| 75 | side: "right", |
| 76 | createdAt: timestamp, |
| 77 | commitOid: "abcdef", |
| 78 | resolvedAt: null, |
| 79 | }; |
| 80 | const capabilities = (overrides: Record<string, boolean> = {}) => ({ |
| 81 | edit: false, |
| 82 | hide: false, |
| 83 | unhide: false, |
| 84 | delete: false, |
| 85 | resolve: false, |
| 86 | unresolve: false, |
| 87 | ...overrides, |
| 88 | }); |
| 89 | const noReviews = decodeReviews({ items: [], next: null }); |
| 90 | const noCommits = decodeCommits({ items: [], headOid: "abcdef", truncated: false, next: null }); |
| 91 | const activity = ( |
| 92 | comments: ReadonlyArray<Record<string, unknown>>, |
| 93 | extra: { readonly reactions?: unknown; readonly host?: string } = {}, |
| 94 | ) => |
| 95 | GitCafePullRequestJson.toActivity({ |
| 96 | comments: decodeComments({ items: comments, next: null }), |
| 97 | reviews: noReviews, |
| 98 | commits: noCommits, |
| 99 | headOid: "abcdef", |
| 100 | host: extra.host ?? "git.cafe", |
| 101 | ...(extra.reactions === undefined ? {} : { reactions: decodeReactions(extra.reactions) }), |
| 102 | }); |
| 103 | |
| 104 | describe("toChangeRequest", () => { |
| 105 | it("reads draft state, the repository host's URLs and absolute avatars", () => { |
| 106 | const row = GitCafePullRequestJson.toChangeRequest(decodePull(pull), { |
| 107 | repository: "owner/repo", |
| 108 | host: "staging.git.cafe", |
| 109 | }); |
| 110 | assert.strictEqual(row.state, "open"); |
| 111 | assert.isTrue(row.isDraft); |
| 112 | assert.strictEqual(row.url, "https://staging.git.cafe/owner/repo/pulls/7"); |
| 113 | assert.deepStrictEqual(row.author, { |
| 114 | login: "alice", |
| 115 | name: null, |
| 116 | avatarUrl: "https://staging.git.cafe/avatars/one", |
| 117 | }); |
| 118 | }); |
| 119 | |
| 120 | it("names linked GitHub reviewers by both logins and drops unavailable ones", () => { |
| 121 | const row = GitCafePullRequestJson.toChangeRequest( |
| 122 | decodePull({ |
| 123 | ...pull, |
| 124 | author: { kind: "github", actorId: "act_remote", login: "remote-user", avatarUrl: null }, |
| 125 | reviewers: [ |
| 126 | { actor }, |
| 127 | { |
| 128 | actor: { |
| 129 | kind: "github", |
| 130 | actorId: "act_github", |
| 131 | login: "upstream", |
| 132 | avatarUrl: null, |
| 133 | linkedProfile: { handle: "linked-local" }, |
| 134 | }, |
| 135 | }, |
| 136 | { actor: { kind: "unavailable", actorId: "act_deleted" } }, |
| 137 | ], |
| 138 | }), |
| 139 | { repository: "owner/repo", host: "git.cafe" }, |
| 140 | ); |
| 141 | assert.strictEqual(row.author?.login, "remote-user"); |
| 142 | assert.deepStrictEqual(row.reviewRequestLogins, ["alice", "upstream", "linked-local"]); |
| 143 | }); |
| 144 | |
| 145 | it("requires capabilities on a detail response", () => { |
| 146 | assert.throws(() => decodePullDetail(pull)); |
| 147 | }); |
| 148 | }); |
| 149 | |
| 150 | describe("toViewerPermissions", () => { |
| 151 | const permissions = (overrides: Record<string, unknown>) => |
| 152 | GitCafePullRequestJson.toViewerPermissions(decodePullDetail({ ...detail, ...overrides })); |
| 153 | |
| 154 | it("lets an editor rewrite the title and description, and nobody else", () => { |
| 155 | assert.isTrue(permissions({}).editChangeRequest); |
| 156 | assert.isFalse( |
| 157 | permissions({ |
| 158 | capabilities: { comment: true, review: true, merge: true, edit: false, moderate: true }, |
| 159 | }).editChangeRequest, |
| 160 | ); |
| 161 | }); |
| 162 | |
| 163 | it("offers lifecycle actions by state and never a standalone branch update", () => { |
| 164 | assert.deepStrictEqual(permissions({}).actions, ["draft", "close", "merge"]); |
| 165 | assert.deepStrictEqual(permissions({ state: "closed" }).actions, ["reopen"]); |
| 166 | assert.deepStrictEqual(permissions({ state: "merged" }).actions, []); |
| 167 | assert.isTrue(permissions({}).stackRebase); |
| 168 | }); |
| 169 | |
| 170 | it("keeps a locked conversation's composer for moderators only", () => { |
| 171 | const locked = (moderate: boolean) => |
| 172 | permissions({ |
| 173 | lockedAt: timestamp, |
| 174 | capabilities: { comment: true, review: false, merge: false, edit: false, moderate }, |
| 175 | }); |
| 176 | assert.isFalse(locked(false).comment); |
| 177 | assert.isFalse(locked(false).resolve); |
| 178 | assert.isTrue(locked(true).comment); |
| 179 | assert.deepStrictEqual(locked(true).verdicts, []); |
| 180 | }); |
| 181 | |
| 182 | it("offers no composer to a viewer GitCafe won't let comment", () => { |
| 183 | const silent = permissions({ |
| 184 | capabilities: { comment: false, review: true, merge: true, edit: true, moderate: true }, |
| 185 | }); |
| 186 | assert.isFalse(silent.comment); |
| 187 | assert.isFalse(silent.resolve); |
| 188 | }); |
| 189 | }); |
| 190 | |
| 191 | describe("toActivity", () => { |
| 192 | it("groups line comments by their persisted thread id and marks a paged list truncated", () => { |
| 193 | const result = GitCafePullRequestJson.toActivity({ |
| 194 | comments: decodeComments({ |
| 195 | items: [ |
| 196 | { ...lineComment, id: "reply", threadId: "thread-one" }, |
| 197 | { ...lineComment, id: "other", threadId: "thread-two" }, |
| 198 | ], |
| 199 | next: "next", |
| 200 | }), |
| 201 | reviews: noReviews, |
| 202 | commits: noCommits, |
| 203 | headOid: "abcdef", |
| 204 | host: "git.cafe", |
| 205 | }); |
| 206 | assert.deepStrictEqual(result.reviewThreads.map((thread) => thread.id).toSorted(), [ |
| 207 | "thread-one", |
| 208 | "thread-two", |
| 209 | ]); |
| 210 | assert.isTrue(result.commentsTruncated); |
| 211 | }); |
| 212 | |
| 213 | it.each([timestamp, null])("takes the thread's resolution from its root: %s", (resolvedAt) => { |
| 214 | const result = activity([ |
| 215 | { ...lineComment, id: "root", threadId: "root", resolvedAt }, |
| 216 | { ...lineComment, id: "reply", threadId: "root", resolvedAt: null }, |
| 217 | ]); |
| 218 | assert.strictEqual(result.reviewThreads.length, 1); |
| 219 | assert.strictEqual(result.reviewThreads[0]?.isResolved, resolvedAt !== null); |
| 220 | assert.strictEqual(result.reviewThreads[0]?.comments.length, 2); |
| 221 | }); |
| 222 | |
| 223 | it("carries per-remark edit permission onto both copies of a remark", () => { |
| 224 | const result = activity([ |
| 225 | { |
| 226 | ...lineComment, |
| 227 | id: "root", |
| 228 | threadId: "root", |
| 229 | capabilities: capabilities({ edit: false, resolve: true }), |
| 230 | }, |
| 231 | { |
| 232 | ...lineComment, |
| 233 | id: "reply", |
| 234 | threadId: "root", |
| 235 | capabilities: capabilities({ edit: true }), |
| 236 | }, |
| 237 | { ...lineComment, id: "silent", threadId: "silent", path: null, line: null, side: null }, |
| 238 | ]); |
| 239 | const canEdit = (id: string) => result.comments.find((comment) => comment.id === id)?.canEdit; |
| 240 | assert.strictEqual(canEdit("root"), false); |
| 241 | assert.strictEqual(canEdit("reply"), true); |
| 242 | // A host that says nothing about a remark leaves the page to guess. |
| 243 | assert.isUndefined(canEdit("silent")); |
| 244 | assert.deepStrictEqual( |
| 245 | result.reviewThreads[0]?.comments.map((comment) => [comment.id, comment.canEdit]), |
| 246 | [ |
| 247 | ["root", false], |
| 248 | ["reply", true], |
| 249 | ], |
| 250 | ); |
| 251 | }); |
| 252 | |
| 253 | it.each([ |
| 254 | [null, { resolve: true, unresolve: false }, true], |
| 255 | [null, { resolve: false, unresolve: true }, false], |
| 256 | [timestamp, { resolve: true, unresolve: false }, false], |
| 257 | [timestamp, { resolve: false, unresolve: true }, true], |
| 258 | ] as const)( |
| 259 | "reads canResolve for a thread resolved at %s with %o as %s", |
| 260 | (resolvedAt, allowed, canResolve) => { |
| 261 | const result = activity([ |
| 262 | { |
| 263 | ...lineComment, |
| 264 | id: "root", |
| 265 | threadId: "root", |
| 266 | resolvedAt, |
| 267 | capabilities: capabilities(allowed), |
| 268 | }, |
| 269 | ]); |
| 270 | assert.strictEqual(result.reviewThreads[0]?.canResolve, canResolve); |
| 271 | }, |
| 272 | ); |
| 273 | |
| 274 | it("leaves canResolve absent when the root says nothing", () => { |
| 275 | const result = activity([{ ...lineComment, id: "root", threadId: "root" }]); |
| 276 | assert.isUndefined(result.reviewThreads[0]?.canResolve); |
| 277 | }); |
| 278 | |
| 279 | it("maps review verdicts, dismissals and commits", () => { |
| 280 | const result = GitCafePullRequestJson.toActivity({ |
| 281 | comments: decodeComments({ items: [], next: null }), |
| 282 | reviews: decodeReviews({ |
| 283 | items: [ |
| 284 | { |
| 285 | id: "r1", |
| 286 | author: actor, |
| 287 | body: "LGTM", |
| 288 | verdict: "approve", |
| 289 | dismissedAt: null, |
| 290 | createdAt: timestamp, |
| 291 | }, |
| 292 | { |
| 293 | id: "r2", |
| 294 | author: actor, |
| 295 | body: null, |
| 296 | verdict: "request_changes", |
| 297 | dismissedAt: timestamp, |
| 298 | createdAt: "2026-09-12T13:00:00Z", |
| 299 | }, |
| 300 | ], |
| 301 | next: null, |
| 302 | }), |
| 303 | commits: decodeCommits({ |
| 304 | items: [{ oid: "abcdef", summary: "Change", time: 1_789_000_000 }], |
| 305 | headOid: "abcdef", |
| 306 | truncated: false, |
| 307 | next: null, |
| 308 | }), |
| 309 | host: "git.cafe", |
| 310 | }); |
| 311 | assert.deepStrictEqual( |
| 312 | result.comments.map((comment) => [comment.id, comment.kind, comment.reviewState]), |
| 313 | [ |
| 314 | ["r1", "review", "approved"], |
| 315 | ["r2", "review", "dismissed"], |
| 316 | ], |
| 317 | ); |
| 318 | assert.deepStrictEqual( |
| 319 | result.commits.map((commit) => commit.oid), |
| 320 | ["abcdef"], |
| 321 | ); |
| 322 | }); |
| 323 | |
| 324 | it("maps supported reactions to the pull request and to matching comments in both views", () => { |
| 325 | const result = activity( |
| 326 | [ |
| 327 | { ...lineComment, id: "comment-a", threadId: "comment-a" }, |
| 328 | { ...lineComment, id: "comment-b", threadId: "comment-a" }, |
| 329 | ], |
| 330 | { |
| 331 | reactions: { |
| 332 | next: null, |
| 333 | items: [ |
| 334 | { |
| 335 | subject: { kind: "pull_request_comment", id: "comment-b" }, |
| 336 | emoji: { kind: "unicode", value: "👍" }, |
| 337 | count: 3, |
| 338 | viewerReactionId: "reaction-viewer", |
| 339 | reactors: [actor, { kind: "unavailable", actorId: "deleted" }], |
| 340 | }, |
| 341 | { |
| 342 | subject: { kind: "pull_request", id: "pr_one" }, |
| 343 | emoji: { kind: "unicode", value: "🚀" }, |
| 344 | count: 1, |
| 345 | viewerReactionId: null, |
| 346 | reactors: [actor], |
| 347 | }, |
| 348 | // Neither a custom emoji nor one outside the shared set is shown under another name. |
| 349 | { |
| 350 | subject: { kind: "pull_request", id: "pr_one" }, |
| 351 | emoji: { kind: "custom", id: "party-parrot" }, |
| 352 | count: 2, |
| 353 | viewerReactionId: null, |
| 354 | reactors: [actor], |
| 355 | }, |
| 356 | { |
| 357 | subject: { kind: "pull_request", id: "pr_one" }, |
| 358 | emoji: { kind: "unicode", value: "🔥" }, |
| 359 | count: 1, |
| 360 | viewerReactionId: null, |
| 361 | reactors: [actor], |
| 362 | }, |
| 363 | ], |
| 364 | }, |
| 365 | }, |
| 366 | ); |
| 367 | const thumbsUp = { |
| 368 | content: "thumbs-up", |
| 369 | count: 3, |
| 370 | actors: ["alice"], |
| 371 | viewerHasReacted: true, |
| 372 | } as const; |
| 373 | assert.deepStrictEqual(result.reactions, [ |
| 374 | { content: "rocket", count: 1, actors: ["alice"], viewerHasReacted: false }, |
| 375 | ]); |
| 376 | assert.deepStrictEqual( |
| 377 | result.comments.map((comment) => [comment.id, comment.reactions]), |
| 378 | [ |
| 379 | ["comment-a", []], |
| 380 | ["comment-b", [thumbsUp]], |
| 381 | ], |
| 382 | ); |
| 383 | assert.deepStrictEqual( |
| 384 | result.reviewThreads[0]?.comments.map((comment) => [comment.id, comment.reactions]), |
| 385 | [ |
| 386 | ["comment-a", []], |
| 387 | ["comment-b", [thumbsUp]], |
| 388 | ], |
| 389 | ); |
| 390 | }); |
| 391 | }); |
| 392 | |
| 393 | describe("toStack", () => { |
| 394 | it("orders members by position and reports heads only where GitCafe gives them", () => { |
| 395 | const { stack } = decodeStackEnvelope({ |
| 396 | stack: { |
| 397 | id: "stack-one", |
| 398 | number: 3, |
| 399 | revision: 1, |
| 400 | landingBase: "main", |
| 401 | members: [ |
| 402 | { |
| 403 | pullRequestNumber: 7, |
| 404 | title: "Top", |
| 405 | state: "open", |
| 406 | draft: true, |
| 407 | sourceBranch: "feature", |
| 408 | position: 2, |
| 409 | }, |
| 410 | { |
| 411 | pullRequestNumber: 6, |
| 412 | title: "Base", |
| 413 | state: "merged", |
| 414 | draft: false, |
| 415 | sourceBranch: "base", |
| 416 | headOid: "0123456789abcdef0123456789abcdef01234567", |
| 417 | position: 1, |
| 418 | }, |
| 419 | ], |
| 420 | }, |
| 421 | }); |
| 422 | assert.isNotNull(stack); |
| 423 | const result = GitCafePullRequestJson.toStack(stack!, { |
| 424 | repository: "owner/repo", |
| 425 | host: "staging.git.cafe", |
| 426 | }); |
| 427 | assert.strictEqual(result.url, "https://staging.git.cafe/owner/repo/stacks/3"); |
| 428 | assert.strictEqual(result.base, "main"); |
| 429 | assert.deepStrictEqual(result.layers, [ |
| 430 | { |
| 431 | number: 6, |
| 432 | title: "Base", |
| 433 | headBranch: "base", |
| 434 | state: "merged", |
| 435 | isDraft: false, |
| 436 | headSha: "0123456789abcdef0123456789abcdef01234567", |
| 437 | }, |
| 438 | { number: 7, title: "Top", headBranch: "feature", state: "open", isDraft: true }, |
| 439 | ]); |
| 440 | }); |
| 441 | |
| 442 | it("reads an unstacked pull request as no stack", () => { |
| 443 | assert.isNull(decodeStackEnvelope({ stack: null }).stack); |
| 444 | }); |
| 445 | }); |
| 446 | |
| 447 | describe("toDiff", () => { |
| 448 | it("converts numeric hunk coordinates into a unified patch", () => { |
| 449 | const diff = decodeDiff({ |
| 450 | items: [ |
| 451 | { |
| 452 | path: "file.ts", |
| 453 | oldPath: null, |
| 454 | status: "modified", |
| 455 | additions: 1, |
| 456 | deletions: 1, |
| 457 | binary: false, |
| 458 | hunks: [ |
| 459 | { |
| 460 | oldStart: 1, |
| 461 | oldLines: 1, |
| 462 | newStart: 1, |
| 463 | newLines: 1, |
| 464 | lines: [ |
| 465 | { origin: "-", content: "old\n" }, |
| 466 | { origin: "+", content: "new\n" }, |
| 467 | ], |
| 468 | }, |
| 469 | ], |
| 470 | }, |
| 471 | ], |
| 472 | truncated: false, |
| 473 | }); |
| 474 | assert.include(GitCafePullRequestJson.toDiff(diff).patch, "@@ -1,1 +1,1 @@\n-old\n+new\n"); |
| 475 | }); |
| 476 | |
| 477 | it("marks an omitted patch incomplete even when GitCafe's own flag is false", () => { |
| 478 | const diff = decodeDiff({ |
| 479 | items: [{ path: "large.ts", oldPath: null, status: "modified", tooLarge: true, hunks: [] }], |
| 480 | truncated: false, |
| 481 | }); |
| 482 | const result = GitCafePullRequestJson.toDiff(diff); |
| 483 | assert.isTrue(result.truncated); |
| 484 | assert.deepStrictEqual(result.omittedFileStats, [ |
| 485 | { path: "large.ts", additions: 0, deletions: 0 }, |
| 486 | ]); |
| 487 | }); |
| 488 | |
| 489 | it("quotes renamed and binary paths", () => { |
| 490 | const { patch } = GitCafePullRequestJson.toDiff( |
| 491 | decodeDiff({ |
| 492 | items: [ |
| 493 | { |
| 494 | path: "new image.png", |
| 495 | oldPath: "old image.png", |
| 496 | status: "renamed", |
| 497 | binary: true, |
| 498 | hunks: [], |
| 499 | }, |
| 500 | ], |
| 501 | truncated: false, |
| 502 | }), |
| 503 | ); |
| 504 | // Git quotes a name for the bytes a header can't carry, not for a space. |
| 505 | assert.include(patch, "rename from old image.png"); |
| 506 | assert.include(patch, "Binary files a/old image.png and b/new image.png differ"); |
| 507 | }); |
| 508 | |
| 509 | it("C-escapes a name whose newline or tab would break the patch headers", () => { |
| 510 | const { patch } = GitCafePullRequestJson.toDiff( |
| 511 | decodeDiff({ |
| 512 | items: [{ path: "two\nlines\tname.ts", status: "added", hunks: [] }], |
| 513 | truncated: false, |
| 514 | }), |
| 515 | ); |
| 516 | assert.include(patch, 'diff --git "a/two\\nlines\\tname.ts" "b/two\\nlines\\tname.ts"'); |
| 517 | assert.notInclude(patch, "two\nlines"); |
| 518 | }); |
| 519 | }); |
| 520 | |
| 521 | describe("toChecks", () => { |
| 522 | it("maps GitCafe's status and conclusion onto the shared check states", () => { |
| 523 | const check = (status: string, conclusion: string | null) => ({ |
| 524 | name: `${status}-${conclusion}`, |
| 525 | status, |
| 526 | conclusion, |
| 527 | summary: null, |
| 528 | detailsUrl: null, |
| 529 | }); |
| 530 | const checks = GitCafePullRequestJson.toChecks( |
| 531 | decodeChecks({ |
| 532 | items: [ |
| 533 | check("queued", null), |
| 534 | check("in_progress", null), |
| 535 | check("completed", "success"), |
| 536 | check("completed", "action_required"), |
| 537 | check("completed", "timed_out"), |
| 538 | ], |
| 539 | next: null, |
| 540 | }), |
| 541 | ); |
| 542 | assert.deepStrictEqual( |
| 543 | checks.map((item) => item.status), |
| 544 | ["pending", "pending", "success", "action-required", "failure"], |
| 545 | ); |
| 546 | }); |
| 547 | }); |