| @@ -37,15 +37,40 @@ struct BuildCancelTests { |
| 37 | 37 | |
| 38 | 38 | @Test func cancelSendsTheBuildNumber() async throws { |
| 39 | 39 | let (client, stub) = try makeClient() |
| 40 | | // The view model's own load response, then the write, then a reload. |
| 40 | // The write, then its reload — `build show` and `build log` fire |
| 41 | // concurrently, so each needs its own matched stub. |
| 41 | 42 | let model = BuildDetailViewModel(client: client, repoPath: "krz/gitbay", number: 966) |
| 42 | 43 | stub.enqueue(.init(status: 200, json: ok)) |
| 43 | | stub.enqueue(.init(status: 200, json: ok)) |
| 44 | stub.enqueue(.init(status: 200, json: """ |
| 45 | {"protocol_version":1,"data":{"number":966,"job":"test","status":"cancelled",\ |
| 46 | "sha":"abc123","ref":"refs/heads/main","created_at":"2026-01-01T00:00:00Z"},\ |
| 47 | "exit_code":0} |
| 48 | """, match: "argv=show")) |
| 49 | stub.enqueue(.init(status: 200, json: ok, match: "argv=log")) |
| 44 | 50 | await model.cancel() |
| 45 | 51 | |
| 46 | 52 | let write = try #require(stub.seen.first { $0.method == "POST" }) |
| 47 | 53 | #expect(try argvOf(write) == ["build", "cancel", "krz/gitbay", "966"]) |
| 48 | 54 | } |
| 55 | |
| 56 | /// The view reads the instance property, not the static function — |
| 57 | /// nothing pinned that it tracks the loaded build, or that it starts |
| 58 | /// out false before anything has loaded. |
| 59 | @Test func instanceIsCancellableTracksTheLoadedBuild() async throws { |
| 60 | let (client, stub) = try makeClient() |
| 61 | let model = BuildDetailViewModel(client: client, repoPath: "krz/gitbay", number: 966) |
| 62 | #expect(model.isCancellable == false) |
| 63 | |
| 64 | stub.enqueue(.init(status: 200, json: """ |
| 65 | {"protocol_version":1,"data":{"number":966,"job":"test","status":"running",\ |
| 66 | "sha":"abc123","ref":"refs/heads/main","created_at":"2026-01-01T00:00:00Z"},\ |
| 67 | "exit_code":0} |
| 68 | """, match: "argv=show")) |
| 69 | stub.enqueue(.init(status: 200, json: ok, match: "argv=log")) |
| 70 | await model.load() |
| 71 | |
| 72 | #expect(model.isCancellable) |
| 73 | } |
| 49 | 74 | } |
| 50 | 75 | |
| 51 | 76 | @MainActor |
| @@ -74,6 +99,27 @@ struct ReleaseDeleteTests { |
| 74 | 99 | #expect(deleted == false) |
| 75 | 100 | #expect(model.actionError?.isEmpty == false) |
| 76 | 101 | } |
| 102 | |
| 103 | /// A stale `actionError` surviving an unrelated later call has been the |
| 104 | /// bug five times over; every `perform` clears it up front, but nothing |
| 105 | /// pinned that. |
| 106 | @Test func actionErrorClearsOnALaterSuccess() async throws { |
| 107 | let (client, stub) = try makeClient() |
| 108 | let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0") |
| 109 | stub.enqueue(.init(status: 200, json: """ |
| 110 | {"protocol_version":1,"error":"deleting a release needs write access","exit_code":4} |
| 111 | """)) |
| 112 | let firstDelete = await model.delete() |
| 113 | |
| 114 | #expect(firstDelete == false) |
| 115 | #expect(model.actionError != nil) |
| 116 | |
| 117 | stub.enqueue(.init(status: 200, json: ok)) |
| 118 | let secondDelete = await model.delete() |
| 119 | |
| 120 | #expect(secondDelete) |
| 121 | #expect(model.actionError == nil) |
| 122 | } |
| 77 | 123 | } |
| 78 | 124 | |
| 79 | 125 | @MainActor |
| @@ -130,8 +176,9 @@ struct OrgCreateRenameTests { |
| 130 | 176 | stub.enqueue(.init(status: 200, json: ok)) |
| 131 | 177 | stub.enqueue(.init(status: 200, json: ok)) |
| 132 | 178 | let model = ProfileViewModel(client: client, name: "cmc") |
| 133 | | _ = await model.createOrg("newco") |
| 179 | let created = await model.createOrg("newco") |
| 134 | 180 | |
| 181 | #expect(created) |
| 135 | 182 | let write = try #require(stub.seen.first { $0.method == "POST" }) |
| 136 | 183 | #expect(try argvOf(write) == ["org", "create", "newco"]) |
| 137 | 184 | } |