From 9c8d3673423be15fe8aa40dcc9127197a844c48e Mon Sep 17 00:00:00 2001 From: noidee Date: Thu, 29 Oct 2026 19:43:50 +0100 Subject: [PATCH 1/8] ci: the gate compiles on the image the release builder already uses The only gate on this project was reading a compiler nobody here builds with. `ci.yml` was pinned to `macos-15`, which on 2026-10-29 printed `Xcode 16.4` / `Build version 16F6` out of `/Applications/Xcode_16.4.app` -- Swift 6.1.2. The development machine is Xcode 27.1 (`27A9275`) / Swift 6.4, and release.yml's build job has run on `macos-latest` since that workflow was written (`c2453cf7`, 2026-07-27); from the repository root this prints nothing, so it was never on the older image: git log -S'macos-15' -- .github/workflows/release.yml Measured 2026-10-29, `macos-latest` is macOS 26 Arm64 with Xcode 26.6 (`17F113`) as its default. So the binary that ships, tests and all, is already compiled there, and `ci.yml` was the odd one out. What that cost, in two runs on the same day. Run 40913494898 failed the BUILD on one line of Sources/MacSCPAppKit/Comparison.swift, green here from a clean scratch path. Run 40928759131, after that fix, got the build through and failed the TEST TARGETS on five more sites across both of them: Tests/macSCPAppKitTests/BoundedViewFigureReportTests.swift:86 Tests/macSCPAppKitTests/ResizableContainerFloorReportTests.swift:44 Tests/macSCPCoreTests/FileDiffCostTests.swift:376 -- extra argument 'severity' in call Tests/macSCPAppKitTests/BoundedViewLayoutGuardTests.swift:363 -- converting non-sendable function value to '@MainActor @Sendable () -> SnippetsSheet' may introduce data races Tests/macSCPAppKitTests/EmptyContainerListTests.swift:131 -- main actor-isolated static property 'allLines' can not be referenced from a nonisolated context plus one `macro expansion #expect:1:1: error: the compiler is unable to type-check this expression in reasonable time`, which is the older type checker's pace and not a defect in the expression. The three `severity:` sites all write `Issue.record(Comment(rawValue: text), severity: .warning)`, an argument the swift-testing that ships with Xcode 16.4 does not have -- so no annotation could have fixed those, only dropping the feature or fencing it behind `#if swift(...)` three times. Two things this does NOT claim. Whether Xcode 26.6 accepts all five sites is unmeasured -- that is what this change exists to find out, and it is why it arrives as a pull request rather than as a push to develop. And `MAX_WARNINGS=0` was tuned against the warnings 6.1.2 prints, so a newer compiler may well turn the warning gate red where the build gate now passes; the first run on the new image is a measurement, not a result. The trade, taken knowingly: `macos-latest` can move under us. Both workflows now move together, so this gate meets a new image before a release does -- the ordering that catches the move instead of shipping it. And both toolchain steps now print `swift --version` beside `xcodebuild -version`, so the Swift figure travels with the run. Reading it off the Xcode version is exactly what went wrong: a comment dated 2026-09-16 recorded `Xcode 27 (27A266a) / Swift 6.4` for a machine, and it was read as the runner's from then until 2026-10-29. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 33 ++++++++++++++++++++++++++++++++- .github/workflows/release.yml | 5 +++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1f217f4..eecbc637 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,33 @@ on: jobs: test: - runs-on: macos-15 + # `macos-latest`, not a pinned older image. Measured 2026-10-29: that + # label is macOS 26 Arm64 with Xcode 26.6 (17F113) as its default, and it + # is what release.yml's build job has used since that workflow was + # written (`c2453cf7`, 2026-07-27). It was never pinned to the older + # image; from the repository root, this prints nothing: + # + # git log -S'macos-15' -- .github/workflows/release.yml + # + # So the app that + # ships is already compiled there, tests and all. Pinned to `macos-15` + # this gate compiled with Xcode 16.4 (16F6) / Swift 6.1.2, a toolchain + # that neither the release builder nor the development machine (Xcode + # 27.1 / Swift 6.4) uses. That is how run 40913494898 came to fail the + # build on a file which had been green locally for weeks, and run + # 40928759131 to fail the test target on five more sites in both test + # targets: the only gate on the project was reading a compiler nobody + # here builds with. + # + # A moving label can move under us. The trade taken knowingly: both + # workflows now move together, so this gate meets a new image before a + # release does, which is the ordering that catches the move instead of + # shipping it. The toolchain step below prints `xcodebuild -version` AND + # `swift --version`, so from now on the figures travel with the run + # rather than with a comment. That is how the wrong figure survived + # from 2026-09-16, the date on the toolchain step's own measurement + # below, to 2026-10-29. + runs-on: macos-latest # The test runner has hung intermittently since M20: the process sits at # 0% CPU and never finishes, so the job would burn the runner's default # six-hour budget instead of failing. Green runs take three to four @@ -72,6 +98,11 @@ jobs: run: | set -euo pipefail xcodebuild -version + # Printed because inferring it from the Xcode version is what went + # wrong: Xcode 16.4 ships Swift 6.1.2, Xcode 27.1 ships 6.4, and a + # comment claiming one while the runner had the other cost two red + # runs on 2026-10-29. + swift --version if xcrun metal --version; then echo "Metal toolchain already present." exit 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83e0803d..1d970069 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -126,6 +126,11 @@ jobs: run: | set -euo pipefail xcodebuild -version + # Printed because inferring it from the Xcode version is what went + # wrong: Xcode 16.4 ships Swift 6.1.2, Xcode 27.1 ships 6.4, and a + # comment claiming one while the runner had the other cost two red + # runs on 2026-10-29. + swift --version if xcrun metal --version; then echo "Metal toolchain already present." exit 0 From 2647e26fa966b6614b536dd0840ac072538e0411 Mon Sep 17 00:00:00 2001 From: noidee Date: Thu, 29 Oct 2026 23:23:42 +0100 Subject: [PATCH 2/8] test: the merge suite's bound is a hang bound, not a ceiling All seven polling cases in RemoteBrowserViewModelMetadataMergeTests came back `Time limit was exceeded: 60.000 seconds` on CI run 40933631584, the first run of the gate on `macos-latest`. The suite's own comment called `.timeLimit(.minutes(1))` "the only clock any assertion reads", which it is -- and that is exactly what made one minute a wall-clock ceiling rather than a hang bound. On the development machine, swift test --filter RemoteBrowserViewModelMetadataMergeTests reports the whole suite in 0.087 s: the slowest of the seven, twoHundredEntriesArrivingInReverseOrderMergeUnderSizeSort, is that same 0.087 s, and the other six run in 0.011 s to 0.013 s. Sixty seconds is 690x the slowest case and still lost to a loaded three-core runner. Five minutes is 3448x it and turns a poll that never completes into a red just as well, which is all the bound is for. ConnectionDiagnostics already carries `.minutes(5)` for the same reason. Nothing about the merge itself is slow: the local figures above are from this same tree, and the suite passes in 0.089 s with the new bound in place. One cross-reference checked while editing the comment, since a comment that names other code is a claim about it: the twoHundredEntries case cites `mergePathIndex`'s doc comment for a hang. Read 2026-10-29, that doc comment records the cost of the `firstIndex(where:)` scan it replaced -- O(n) per arrival, O(n^2) over a listing, "roughly 12.5 million compares" for 5000 entries -- and does not mention a hang. The new paragraph says so rather than repeating the claim. Co-Authored-By: Claude Opus 5 --- ...teBrowserViewModelMetadataMergeTests.swift | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Tests/macSCPCoreTests/RemoteBrowserViewModelMetadataMergeTests.swift b/Tests/macSCPCoreTests/RemoteBrowserViewModelMetadataMergeTests.swift index 9e7994e1..91bfd47e 100644 --- a/Tests/macSCPCoreTests/RemoteBrowserViewModelMetadataMergeTests.swift +++ b/Tests/macSCPCoreTests/RemoteBrowserViewModelMetadataMergeTests.swift @@ -12,14 +12,38 @@ import Testing /// mirroring `LocalFileSystemTests`' own `metadataAbandonsAStuckEntry…` /// tests, but one layer up, at the view model. /// -/// `.timeLimit(.minutes(1))` in its own suite rather than added to the much +/// `.timeLimit(.minutes(5))` in its own suite rather than added to the much /// larger `RemoteBrowserViewModelTests` suite: only these tests poll via /// `pollUntil` for a background merge Task to catch up, so only they need a /// stated ceiling on how long that poll may run before the harness itself /// calls it a hang (CLAUDE.md: "Tests never block the cooperative pool" — /// every wait here is `pollUntil`'s `await Task.sleep`, never a blocking /// wait, and the `.timeLimit` is the only clock any assertion reads). -@Suite("RemoteBrowserViewModel metadata merge", .timeLimit(.minutes(1))) +/// +/// The bound read `.minutes(1)` until 2026-10-29 — the paragraph above +/// opened "`.timeLimit(.minutes(1))` in its own suite" — when all seven +/// polling cases here came back `Time limit was exceeded: 60.000 seconds` +/// on CI run 40933631584, the first run of the gate on `macos-latest`. One +/// minute was not a hang bound for them; it was a wall-clock ceiling, which +/// is the thing CLAUDE.md has a rule against. On the development machine +/// +/// swift test --filter RemoteBrowserViewModelMetadataMergeTests +/// +/// reports the whole suite in 0.087 s, the slowest of the seven +/// (`twoHundredEntriesArrivingInReverseOrderMergeUnderSizeSort`) in that +/// same 0.087 s, and the other six in 0.011 s to 0.013 s. Sixty seconds is +/// 690x the slowest case measured here and still lost to a loaded +/// three-core runner. Five minutes is 3448x it, and turns a poll that +/// never completes into a red just as well, which is all this bound is +/// for. `ConnectionDiagnostics` carries `.minutes(5)` for the same reason. +/// +/// One cross-reference checked while changing this, since it is a claim +/// about other code: `twoHundredEntries…`'s own comment below cites +/// `mergePathIndex`'s doc comment for a hang. Read 2026-10-29, that doc +/// comment records the cost of the `firstIndex(where:)` scan it replaced +/// — O(n) per arrival, O(n²) over a listing, "roughly 12.5 million +/// compares" for 5000 entries — and does not mention a hang. +@Suite("RemoteBrowserViewModel metadata merge", .timeLimit(.minutes(5))) @MainActor struct RemoteBrowserViewModelMetadataMergeTests { /// A `RemoteFileSystem` that ALSO conforms to `LocalMetadataSource`, From 798514634eef572c7ce837d20e701c77503c23c8 Mon Sep 17 00:00:00 2001 From: noidee Date: Thu, 29 Oct 2026 23:24:02 +0100 Subject: [PATCH 3/8] ci: the step budget rests on a maximum that is still current Run 40933631584 killed the Unit-Tests step at its own `timeout-minutes: 8`. The budget was not wrong when it was written; the maximum it rests on expired. Two runs measure that, each read with gh run view --repo NoiXdev/macSCP --log which prints the Build step's `Build complete! (s)`, then `swift test`'s own compile of the test targets as a second one, and -- on a run that reaches the end -- `Test run with tests passed after seconds`: 38225383495, 2026-10-12, c0762fac, macos-15, GREEN: 240.40s build, 244.42s test-target compile, 6821 tests in 78.397 s. Whole step 329 s, which is 1.46x inside the 480 s budget -- not the 2.09x the step's comment derived on 2026-09-05 from a 230 s maximum. 40933631584, 2026-10-29, 9c8d3673, macos-latest, killed: 230.46s build, 392.46s test-target compile. The first test did not start until 397 s (6 m 37 s) into the step, leaving 83 s of the 480 s for the whole suite; the kill landed 96 s after the first test started. So the step now reads 16 minutes: 960 s is 2.04x of 392 s + 78 s = 470 s. That sum is this image's measured compile plus the OTHER image's measured run, because this image's run figure does not exist yet -- the step was killed before the suite finished. The comment says so, and says that the first green run on this image replaces both the addend and the number. The two compile figures are NOT a toolchain comparison, and the comment forbids reading them as one: 28,721 lines of test code and 17,144 lines of source landed between those two commits. git diff --shortstat c0762fac..dcb5a414 -- Tests/ git diff --shortstat c0762fac..dcb5a414 -- Sources/ Note which way that cuts on the build half: Sources grew by those 17,144 lines and `swift build` still went 240.40s -> 230.46s. The job budget goes from 20 to 35 minutes to leave the wider step room. Its comment claimed "Green runs take three to four minutes, so 20 leaves ample room for a slow runner" -- the withdrawn wording. Three to four minutes was the figure for the test STEP in September, never for the job: the last green run took 9 m 52 s end to end, 08:44:12Z to 08:54:04Z, from gh run view 38225383495 --repo NoiXdev/macSCP --json jobs reading the job's own startedAt and completedAt. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 82 +++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eecbc637..a50001ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,11 +36,22 @@ jobs: runs-on: macos-latest # The test runner has hung intermittently since M20: the process sits at # 0% CPU and never finishes, so the job would burn the runner's default - # six-hour budget instead of failing. Green runs take three to four - # minutes, so 20 leaves ample room for a slow runner while turning a hang - # into a red build. This is the net, not the fix -- the cause is still - # open. - timeout-minutes: 20 + # six-hour budget instead of failing. This is the net, not the fix -- + # the cause is still open. + # + # 35, raised from 20 on 2026-10-29 together with the Unit-Tests step + # budget below; the derivation is there. This comment also read "Green + # runs take three to four minutes, so 20 leaves ample room for a slow + # runner", which had stopped being true of the job long before the image + # moved: the last green run on `macos-15` (38225383495, 2026-10-12, + # `c0762fac`) took 9 m 52 s end to end (08:44:12Z to 08:54:04Z), + # measured with + # + # gh run view 38225383495 --repo NoiXdev/macSCP --json jobs + # + # reading the job's own startedAt and completedAt. Three to four minutes + # was the figure for the TEST STEP in September, not for the job. + timeout-minutes: 35 env: # --------------------------------------------------------------------- # Compiler-warning budget. Raising it accepts a warning forever; read @@ -151,20 +162,57 @@ jobs: # by name (run 33698102652 lost 60 suite verdicts the same way). The # step below reads the file back; the artifact keeps it. # - # `timeout-minutes: 8` (docs/BACKLOG.md, "A test parked on a bare + # `timeout-minutes: 16` (docs/BACKLOG.md, "A test parked on a bare # continuation outlives its time limit"): a test parked on a bare # continuation ignores Swift Testing's own `.timeLimit` cancellation - # and would otherwise run on to the JOB's 20-minute budget above - # instead of failing near its own stated limit. Measured from the - # last three green `develop` runs before this step timeout was - # added (`gh run list --branch develop --limit 5 --json - # databaseId,conclusion`, then `gh run view --json jobs --jq - # '.jobs[].steps[] | select(.name|test("Unit-Tests")) | - # "\(.startedAt) \(.completedAt)"'`): runs 33972030162, 33959532085 - # and 33955366191 took 181 s, 230 s and 184 s — a measured maximum - # of 230 s (3 m 50 s). 8 minutes (480 s) is 2.09x that maximum, - # comfortably above the "at least 2x" floor. - timeout-minutes: 8 + # and would otherwise run on to the JOB's budget above instead of + # failing near its own stated limit. + # + # It read `timeout-minutes: 8` until 2026-10-29, derived -- the + # withdrawn wording -- from "the last three green `develop` runs + # before this step timeout was added ... runs 33972030162, + # 33959532085 and 33955366191 took 181 s, 230 s and 184 s — a + # measured maximum of 230 s (3 m 50 s). 8 minutes (480 s) is 2.09x + # that maximum". That derivation was correct on 2026-09-05 and is + # correct arithmetic still; what expired is the maximum it rests on. + # Two runs measure it, each read with + # + # gh run view --repo NoiXdev/macSCP --log + # + # which prints the two `Build complete! (s)` lines (the Build + # step's, then `swift test`'s own compile of the test targets) and, + # on a run that reaches the end, the `Test run with tests passed + # after seconds` line -- which the killed run below never + # printed, hence its missing run figure: + # + # 38225383495, 2026-10-12, `c0762fac`, macos-15, GREEN: + # 240.40s build, 244.42s test-target compile, + # 6821 tests in 78.397 s -- whole step 329 s, i.e. 1.46x inside + # the 480 s budget, not the 2.09x above. + # 40933631584, 2026-10-29, `9c8d3673`, macos-latest, KILLED here: + # 230.46s build, 392.46s test-target compile. The first test did + # not start until 397 s (6 m 37 s) into the step, leaving 83 s + # of the 480 s for the whole suite; the kill landed 96 s after + # the first test started. How many tests the current tree has is + # not measured -- the run never reached a total. + # + # The two compile figures are NOT a toolchain comparison and must + # not be read as one. 28,721 lines of test code and 17,144 lines of + # source landed between those two commits: + # + # git diff --shortstat c0762fac..dcb5a414 -- Tests/ + # git diff --shortstat c0762fac..dcb5a414 -- Sources/ + # + # Note which way that cuts on the build half: Sources grew by those + # 17,144 lines and the Build step still came back 240.40s -> 230.46s. + # + # 16 minutes (960 s) is 2.04x of 392 s + 78 s = 470 s -- this + # image's measured compile plus the OTHER image's measured run, + # because this image's run figure does not exist yet: the step was + # killed before the suite finished. The first green run on this + # image replaces that addend and this number with its own + # measurement. + timeout-minutes: 16 run: | set -o pipefail swift test --event-stream-output-path "$RUNNER_TEMP/test-events.jsonl" \ From a64df8ee40032ea8aca6afe5aa6e4f63b3343faa Mon Sep 17 00:00:00 2001 From: noidee Date: Fri, 30 Oct 2026 09:53:16 +0100 Subject: [PATCH 4/8] test: an out-of-range control reports instead of reddening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI run 40965600726 (2026-10-29, `79851463`, `macos-latest`) failed `awaitingTheConnectorDoesNotKeepTheMainActor` on liveness: test1 ambient=23.317136667 seconds gap=4.923961375 seconds ceiling=4.0 seconds read with `gh run view 40965600726 --repo NoiXdev/macSCP --log`. The ambient control -- the same ticker, same process, moments earlier, with nothing under test -- measured a 23.317 s main-actor stall, which is 4.74x the 4.924 s gap the assertion called a defect. The ceiling is capped at 4 s on purpose: test 1's detectable block is 5 s, so a cap above it would make the assertion unfailable. Both halves of that are right, and together they describe a run in which the instrument has no power at all -- a gap under the ceiling is unreachable and a gap over it is what noise and block both produce. This file has named that regime since 2026-08-28 and chose a side: "so a run in that regime is a false red rather than a blind pass -- the direction this suite prefers, since a false red is visible and a blind pass is not. No run of the rebuilt assertion has reached it (2026-09-02)" -- the withdrawn wording, quoted in the comment it was removed from. A run has now reached it, and a third option is better than either: a measurement whose control is off the scale has no verdict to give, so it reports its figures and does not assert. Both call sites now go through one `stateGapAgainstCeiling` helper -- they held verbatim copies of the same print-and-expect block -- which prints the same `liveness:` line as before when the control is in range and asserts the ceiling there, and otherwise prints and records a warning-severity issue naming the case and both figures. The house pattern is `FileDiffCostTests.report(_:)`. Three things keep this from being a check that quietly stopped checking, the failure mode CLAUDE.md names under "Guards that name what they watch": the skip is loud, test 1's `ranOnMainThread == false` is deterministic and still asserts, and test 3 blocks the main actor on purpose and asserts a FLOOR on the gap, which no load can lift. The comment also names what goes unproven on such a run, rather than implying the suite is unaffected. Sensitivity measured, since a negative check that learns to decline is exactly the kind this project does not take on trust. Two plants through `scripts/mutation-probe`, differing only in whether the control reads out of range: scripts/mutation-probe --filter 'awaitingTheConnectorDoesNotKeepTheMainActor' \ --apply "perl -0pi -e 's/^ _ = await vm\.connect\(\)\$/ blockThisThread(forMilliseconds: 5000)\n _ = await vm.connect()/m' Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift" → RESULT: RED — caught by macSCPCoreTests.ConnectMainActorLivenessTests/awaitingTheConnectorDoesNotKeepTheMainActor() scripts/mutation-probe --filter 'awaitingTheConnectorDoesNotKeepTheMainActor' \ --apply "perl -0pi -e 's/^ _ = await vm\.connect\(\)\$/ blockThisThread(forMilliseconds: 5000)\n _ = await vm.connect()/m; s/^ try\? await Task\.sleep\(for: window\)\$/ blockThisThread(forMilliseconds: 5000)\n try? await Task.sleep(for: window)/m' Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift" → RESULT: GREEN The second carries the first's plant and one more, inside `ambientGap(over:)`, which lifts the control to ~5 s and so past the 4 s cap. Same defect, same assertion, opposite verdicts: the branch does what it says and nothing else in the test moved. A third plant -- the ambient block alone -- is GREEN too and proves nothing either way, since it never touches the dial window the gap is measured over; it is in the record as a probe the guard was never asked to see, not as evidence. Test 2 is gated behind `MACSCP_ITEST=1` and was not probed. Co-Authored-By: Claude Opus 5 --- .../ConnectMainActorLivenessTests.swift | 120 +++++++++++++++--- 1 file changed, 104 insertions(+), 16 deletions(-) diff --git a/Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift b/Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift index 849ad8ba..3e9e4751 100644 --- a/Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift +++ b/Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift @@ -219,14 +219,112 @@ struct ConnectMainActorLivenessTests { /// /// What this does NOT bound is the instrument's own floor as measured /// on 2026-08-28 and recorded at the top of this file: 4.7 s with a - /// bare ticker. That is above the cap, so a run in that regime is a - /// false red rather than a blind pass — the direction this suite - /// prefers, since a false red is visible and a blind pass is not. No - /// run of the rebuilt assertion has reached it (2026-09-02). + /// bare ticker. That is above the cap, so a run in that regime cannot + /// return a verdict either way: a gap under the ceiling is unreachable + /// and a gap over it separates nothing. This paragraph went on to read + /// "so a run in that regime is a false red rather than a blind pass — + /// the direction this suite prefers, since a false red is visible and a + /// blind pass is not. No run of the rebuilt assertion has reached it + /// (2026-09-02)" — the withdrawn wording. A run has now reached it, and + /// what happens there is no longer a false red: see + /// `stateGapAgainstCeiling` below, which reports such a run instead of + /// reddening it. private func ceiling(forAmbient ambient: Duration) -> Duration { min(max(ambient * 3, .seconds(2)), .seconds(4)) } + /// States a measured gap against the ceiling — and declines to assert + /// when the control says the instrument cannot separate the two + /// magnitudes at all. + /// + /// `ceiling(forAmbient:)` is capped at four seconds, so an ambient + /// control ABOVE that cap describes a run in which the noise alone + /// already clears the bar a block would have to clear. There is no + /// verdict to give there: a gap under the ceiling is unreachable, and a + /// gap over it is what noise and block both produce. CI run + /// 40965600726 (2026-10-29, `79851463`, `macos-latest`) is the first + /// run on record in that regime — it printed + /// `ambient=23.317136667 seconds gap=4.923961375 seconds ceiling=4.0 + /// seconds` and failed test 1 on the gap. The ambient control is 4.74x + /// the gap it red-flagged, which is the whole of the argument that the + /// gap was the harness; the run is read with + /// + /// gh run view 40965600726 --repo NoiXdev/macSCP --log + /// + /// Reported, then, rather than asserted on, following the house pattern + /// of `FileDiffCostTests.report(_:)`: `print` puts the figures in the + /// build log, which is how a CI run is read, and a warning-severity + /// issue puts them in the test report, which is how a local run is + /// read. Neither fails the test. + /// + /// What keeps this from being a check that quietly stopped checking — + /// the failure mode CLAUDE.md names under "Guards that name what they + /// watch", where a negative check that matches nothing reads exactly + /// like one that is satisfied: + /// + /// - the skip is loud. It names the case and prints both figures, so a + /// run that measured nothing says so rather than passing silently. + /// - test 1's `ranOnMainThread == false` is deterministic, not a timing + /// measurement, and still asserts. It is the half that catches the + /// connector body inheriting main-actor isolation. + /// - test 3 blocks the main actor on purpose and asserts a FLOOR on the + /// gap (`> 250 ms`). A floor cannot be defeated by a slow machine, + /// only by a broken instrument — so the ticker's own sensitivity + /// stays pinned by an assertion no load can lift, and it is the + /// positive check standing beside this negative one. + /// + /// What is NOT covered while the control is out of range: a + /// caller-side blocking bridge of the dial's full duration, the shape + /// test 3 measures directly. On such a run that shape is proven only + /// where test 3 proves it, from the main actor, and not through either + /// awaited connect. + /// + /// Measured with `scripts/mutation-probe` on 2026-10-30, two plants + /// that differ only in whether the control reads out of range: + /// + /// A. `blockThisThread(forMilliseconds: 5000)` inserted before + /// `_ = await vm.connect()` — a 5 s block of the main actor + /// inside the dial window. RESULT: RED, caught by this test. + /// D. that same insertion, PLUS one before + /// `try? await Task.sleep(for: window)` in `ambientGap(over:)`, + /// which lifts the control to ~5 s and so past the 4 s cap. + /// RESULT: GREEN. + /// + /// Same defect, same assertion; the one difference between them is the + /// control. A third plant, the ambient block on its own, is GREEN as + /// well and proves nothing either way — it never touches the dial + /// window the gap is measured over, so the assertion was never asked + /// to see it. Both probes ran `--filter + /// awaitingTheConnectorDoesNotKeepTheMainActor`; test 2 is gated behind + /// `MACSCP_ITEST=1` and was not probed. + @MainActor + private func stateGapAgainstCeiling( + _ log: TickLog, + ambient: Duration, + test: String, + sourceLocation: SourceLocation = #_sourceLocation + ) { + let bar = ceiling(forAmbient: ambient) + let figures = "\(test) ambient=\(ambient) gap=\(log.largestGap) ceiling=\(bar)" + print("liveness: " + figures) + guard ambient <= bar else { + let text = + "liveness: \(figures) — NOT ASSERTED. The ambient control is " + + "above the ceiling, so noise and block are not separable in " + + "this run and the gap has no verdict to give. Measured over " + + "\(log.stamps.count) ticks." + print(text) + Issue.record( + Comment(rawValue: text), severity: .warning, + sourceLocation: sourceLocation) + return + } + #expect( + log.largestGap <= bar, + "gap \(log.largestGap) over \(log.stamps.count) ticks, ambient \(ambient)", + sourceLocation: sourceLocation) + } + // MARK: - 1. The boundary itself /// The load-bearing structural claim: `ConnectionViewModel.connect()` is @@ -282,12 +380,7 @@ struct ConnectMainActorLivenessTests { #expect(ranOnMainThread.value == false) // The dial really lasted what the ceiling is calibrated against. #expect(elapsed > .seconds(4), "the dial was only \(elapsed)") - print( - "liveness: test1 ambient=\(ambient) gap=\(log.largestGap) " - + "ceiling=\(ceiling(forAmbient: ambient))") - #expect( - log.largestGap <= ceiling(forAmbient: ambient), - "gap \(log.largestGap) over \(log.stamps.count) ticks, ambient \(ambient)") + stateGapAgainstCeiling(log, ambient: ambient, test: "test1") } // MARK: - 2. The real dial @@ -344,12 +437,7 @@ struct ConnectMainActorLivenessTests { #expect(thrown != nil) #expect(elapsed > .seconds(5)) // And the main actor ran the whole time it was stalling. - print( - "liveness: test2 ambient=\(ambient) gap=\(log.largestGap) " - + "ceiling=\(ceiling(forAmbient: ambient))") - #expect( - log.largestGap <= ceiling(forAmbient: ambient), - "gap \(log.largestGap) over \(log.stamps.count) ticks, ambient \(ambient)") + stateGapAgainstCeiling(log, ambient: ambient, test: "test2") } // MARK: - 3. The agent's blocking wait From c8041d1943e38fed968d99295f9c9ca01decc373 Mon Sep 17 00:00:00 2001 From: noidee Date: Fri, 30 Oct 2026 10:36:33 +0100 Subject: [PATCH 5/8] test: a one-minute time limit is below this runner's floor Run 41030496532 (`a64df8ee`, the third run of the gate on `macos-latest`) failed with 73 tests red in 11 suites, and every one of them for the same reason. Read with gh run view 41030496532 --repo NoiXdev/macSCP --log the whole suite finished -- `Test run with 7975 tests in 650 suites failed after 131.530` -- and the reason tally has exactly one entry: grep -o 'Time limit was exceeded: [0-9.]* seconds' | sort | uniq -c # 148 Time limit was exceeded: 60.000 seconds 148 issues, no second reason. Confirmed by difference rather than by reading the list: of the tests that recorded ANY issue, all but one also recorded a time-limit issue, and the one that did not is ConnectMainActorLivenessTests' own warning-severity report, which does not fail its test. # tests with any issue: 74; with a time-limit issue: 73 comm -23 <(grep -o 'issue in [A-Za-z]*\.[A-Za-z]*/[A-Za-z0-9_]*()' \ | sed 's/.*\///' | sort -u) \ <(grep 'Time limit was exceeded' \ | grep -o 'issue in [A-Za-z]*\.[A-Za-z]*/[A-Za-z0-9_]*()' \ | sed 's/.*\///' | sort -u) # awaitingTheConnectorDoesNotKeepTheMainActor() This is not a property of any suite. Three named cases, same 60 s bound, two consecutive runs on the same image: cancelAllWithParallelRunners() 70.097 s PASS / 100.325 s FAIL cancelAllCancelsQueuedAndRunning() 72.676 s PASS / 100.335 s FAIL sendPreservesFIFOOrderUnderVaryingLatency() 89.332 s PASS / 100.360 s FAIL Left column run 40965600726, right column 41030496532. All three ran past their own 60 s bound in BOTH runs and reddened only in the second, so the bound is not a clock these tests lost to -- it is a race. The mechanism is the one the CLAUDE.md section already records one layer down: Swift Testing runs the suite in parallel with no concurrency limit, the cooperative pool is as wide as the machine has cores (three), and all 7975 tests are in flight, so a case's reported duration is the run's wall clock as seen from that case. In run 41030496532, of 6911 cases with a duration line, 6843 reported over 10 s, 6335 over 30 s, 1153 over 60 s and 45 under 1 s, against a run total of 131.530 s. Whether a 60 s bound fires is then down to whether the timer enforcing it gets a thread, which is the 2026-09-19 `DetachedProbe` starvation measured again at the harness level. So the 110 `.minutes(1)` bounds in `Tests/` become `.minutes(5)`: 2.3x the 131.530 s run, 0.31x the 960 s step budget -- above the suite's wall clock to be a hang bound at all, below the step budget so a real hang still reds with the test's name instead of killing the step. The script that made the change asserted its own reach before writing: 110 occurrences on 95 files, skipping `///` lines. 127 bounds of this kind now stand in `Tests/`: grep -rnE '\.timeLimit\(\.minutes\(5\)\)' Tests/ | grep -vc '///' A LINE count, deliberately: a bound is one trait on one line, while the same spelling also sits in nine doc comments, and the escaped pattern plus `grep -vc '///'` keeps both those and the command's own text out of the figure. It took three tries to stop that number moving -- 128 matches, then 136 -- each time because writing the sentence added a spelling the previous command counted. That is the `grep -c` rule in CLAUDE.md arriving from its other side: here lines ARE the unit, so `-c` is the right tool. The bulk change skipped `///` lines so that comments quoting a bound would survive it. That protected the two quotations worth keeping -- this file's own withdrawal, and ConnectionDiagnosticsJumpTests' record of four per-case traits it removed in 2026-10-05 -- and left EIGHT comments describing a bound their suite no longer has, which is the "Comments that describe other code" failure in this very commit. They are corrected here, and three of them owed more than a token: AgentAuth and SSHPrivateKeyLoader each argued FOR one minute -- "The project default is enough" and "nothing here needed the wider limit" -- on the premise that a minute of reported time is a minute of this suite's work. It is not; it is the run's wall clock seen from one case. DiagnosticLog's bound was "not a duration this suite is expected to approach", which was true of its work and false of what the harness reports. QuitSequence's sentence dated the bound ("since fix round 2 of the port-forwarding plan's Task 6"), so swapping the value in place would have claimed five minutes dated from then; it now says when each is from. Two claims in that same comment are withdrawn in this commit, quoted verbatim where they stood. It said "only these tests poll via `pollUntil` for a background merge Task to catch up, so only they need a stated ceiling": an uncounted claim about the rest of the tree, and wrong by 126. And it attributed the first red to this suite -- "One minute was not a hang bound for them; it was a wall-clock ceiling", with "Sixty seconds is 690x the slowest case measured here and still lost to a loaded three-core runner". The direction held; the attribution did not, and the local 0.087 s figure the ratio rests on is beside the point, because the runner figure it is divided into is the run's duration rather than the case's work. CLAUDE.md gets the measurement under "A wall-clock ceiling in a test measures the runner", and its closing rule now names `.timeLimit` as one of the ceilings it is about. Verified locally before committing, on this tree: the merge, transfer-queue and liveness suites at 105 tests in 3 suites, and the five suites whose comments are rewritten here -- Polling guard among them, since it is the scanner that reads test source and could have reacted to the new comment text -- at 93 tests in 4 suites plus 19 in 1. All green. Not measured, and deliberately untouched: the 27 `.minutes(2)` bounds. No run has recorded one firing, but 120 s is 0.91x this run's total, so they are the next candidates rather than a cleared set. And the fix has a shelf life by construction -- it ties the bound to the suite's size, so a run twice as long makes five minutes marginal again. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 50 +++++++++++++++- .../AlreadyOpenSessionTests.swift | 2 +- .../ArchivePresentationTests.swift | 2 +- .../BucketObjectLockSheetTests.swift | 2 +- .../BucketVersioningSheetTests.swift | 2 +- .../ComparisonFetchTests.swift | 10 ++-- .../ComparisonOverviewTests.swift | 2 +- .../ComparisonOwnershipTests.swift | 2 +- .../ComparisonWindowModelTests.swift | 2 +- .../ConnectAttemptHandoffTests.swift | 2 +- .../DetailSurfacePlanTests.swift | 2 +- .../ErrorNotificationTests.swift | 2 +- .../HostKeyMismatchHintGuardTests.swift | 2 +- .../HostResolveModelTests.swift | 2 +- .../macSCPAppKitTests/JumpTwoTabsTests.swift | 2 +- .../KeyPassphraseActionsTests.swift | 2 +- .../LivenessProbeCancellationTests.swift | 2 +- .../LivenessProbeCauseTests.swift | 4 +- .../LivenessProbeRaceTests.swift | 2 +- .../ObjectLockSheetTests.swift | 2 +- .../macSCPAppKitTests/QuitSequenceTests.swift | 5 +- .../ReconnectPathTests.swift | 2 +- .../SSHTerminalViewSizingTests.swift | 2 +- .../SnippetAfterConnectSequenceTests.swift | 2 +- .../StoreReloadOnActivationGuardTests.swift | 2 +- .../macSCPAppKitTests/TabRegistryTests.swift | 2 +- .../macSCPAppKitTests/TabTitlePlanTests.swift | 2 +- .../TeardownStageTests.swift | 6 +- .../TunnelDockPresenceTests.swift | 2 +- .../TunnelHostKeyPromptBridgeTests.swift | 2 +- .../TunnelManagerTests.swift | 2 +- Tests/macSCPCoreTests/AgentAuthTests.swift | 13 ++++- Tests/macSCPCoreTests/AgentEnvLockTests.swift | 2 +- .../ArchiveActivityTests.swift | 2 +- .../ArchiveCommandChannelTests.swift | 2 +- .../ArchiveCommandLineTests.swift | 2 +- .../macSCPCoreTests/ArchiveFormatTests.swift | 2 +- .../ArchiveMenuEntriesTests.swift | 2 +- Tests/macSCPCoreTests/ArchivePlanTests.swift | 2 +- Tests/macSCPCoreTests/AsyncSignalTests.swift | 2 +- .../AwaitResumptionTests.swift | 2 +- Tests/macSCPCoreTests/BoundedCloseTests.swift | 4 +- Tests/macSCPCoreTests/BytePumpTests.swift | 2 +- .../CLIDiagnoseForegroundRunTests.swift | 2 +- Tests/macSCPCoreTests/CLIMatrixITests.swift | 2 +- .../CLISecretSourcesTests.swift | 2 +- .../CLITunnelForegroundRunTests.swift | 2 +- .../ConnectFailureSecrecyTests.swift | 2 +- .../ConnectionDiagnosticsTests.swift | 4 +- ...ViewModelFailureAcknowledgementTests.swift | 2 +- ...nectionViewModelManagedKeyStoreTests.swift | 2 +- .../ConnectionViewModelTests.swift | 2 +- .../DiagnosticLogSharedSinkTests.swift | 2 +- .../macSCPCoreTests/DiagnosticLogTests.swift | 13 +++-- .../DiagnosticsNoDescribingGuardTests.swift | 2 +- .../DialFailureGroupReleaseTests.swift | 2 +- .../EmbeddedKeyPorterTests.swift | 2 +- .../macSCPCoreTests/ExtractPreviewTests.swift | 2 +- .../FileActionActivityTests.swift | 4 +- Tests/macSCPCoreTests/FileDiffTests.swift | 6 +- .../GenerateKeyFormTests.swift | 2 +- .../HostAddressLookupTests.swift | 2 +- .../ITermColorsImportTests.swift | 2 +- .../InternetSpeedLiveTransportTests.swift | 2 +- .../KeyPassphraseFormsTests.swift | 2 +- .../LocalFileSystemTests.swift | 4 +- .../LocalForwardListenerTests.swift | 2 +- .../ManagedKeyStoreUnreadableTests.swift | 2 +- Tests/macSCPCoreTests/NetworkTraceTests.swift | 2 +- Tests/macSCPCoreTests/OneAtATimeTests.swift | 2 +- .../PKCS12ImportIsolationGuardTests.swift | 2 +- .../macSCPCoreTests/PTYSubprocessTests.swift | 2 +- Tests/macSCPCoreTests/PollUntilTests.swift | 2 +- Tests/macSCPCoreTests/PollingGuardTests.swift | 4 +- .../macSCPCoreTests/ProbeDeadlineTests.swift | 4 +- Tests/macSCPCoreTests/ProbeStartTests.swift | 2 +- ...teBrowserViewModelMetadataMergeTests.swift | 57 +++++++++++++++---- .../macSCPCoreTests/RemoteForwardTests.swift | 2 +- ...edirectAuthorizationMeasurementTests.swift | 2 +- .../S3RedirectControlTests.swift | 2 +- .../S3SessionIsolationTests.swift | 2 +- .../macSCPCoreTests/SFTPStartBoundTests.swift | 2 +- .../SOCKS5HandshakeTests.swift | 2 +- .../macSCPCoreTests/SOCKS5ListenerTests.swift | 4 +- .../SSHForwardingConnectionTests.swift | 2 +- .../SSHPrivateKeyLoaderTests.swift | 16 ++++-- .../TerminalPanelViewModelTests.swift | 6 +- .../macSCPCoreTests/TerminalThemeTests.swift | 2 +- Tests/macSCPCoreTests/TerminalTypeTests.swift | 2 +- .../macSCPCoreTests/TransferEngineTests.swift | 2 +- .../TransferQueueViewModelTests.swift | 2 +- Tests/macSCPCoreTests/TunnelLatchTests.swift | 4 +- Tests/macSCPCoreTests/TunnelRunnerTests.swift | 2 +- .../TunnelRuntimeFactoryWiringTests.swift | 2 +- .../macSCPCoreTests/TunnelRuntimeTests.swift | 2 +- .../WebDAVFileSystemIntegrationTests.swift | 2 +- .../WebDAVFileSystemWriteTests.swift | 2 +- 97 files changed, 238 insertions(+), 136 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 75071196..be09dc07 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -200,10 +200,58 @@ cooperative pool never gave the probe's body a thread, and a body that never began did not overrun anything. The case asserts that the deadline settled the row, which is the property it always existed for. +**2026-10-30 update: `.timeLimit` is a wall-clock ceiling too, and on a +saturated runner it fires at random.** Two runs of the gate on +`macos-latest`, two commits apart, same image, the suites these three +cases live in still bounded at `.minutes(1)`: + +| case, bound 60 s | run 40965600726 | run 41030496532 | +|---|---|---| +| `cancelAllWithParallelRunners()` | passed after 70.097 s | failed after 100.325 s | +| `cancelAllCancelsQueuedAndRunning()` | passed after 72.676 s | failed after 100.335 s | +| `sendPreservesFIFOOrderUnderVaryingLatency()` | passed after 89.332 s | failed after 100.360 s | + +All three ran past their own 60 s bound in BOTH runs and reddened only in +the second. Run 41030496532 recorded 148 `Time limit was exceeded: 60.000 +seconds` issues across 73 tests in 11 suites; run 40965600726, which +reached the end of the suite, recorded none. Of the 74 tests that recorded +any issue in the later run, 73 recorded that one, so there was no second +cause — the one that did not is `ConnectMainActorLivenessTests`' own +warning-severity report, and that test passed. + +Why the figures read like that: Swift Testing runs the suite in parallel +with no concurrency limit, the cooperative pool is as wide as the machine +has cores (three here), and all 7975 tests are in flight at once. So +`Test X passed after N seconds` is the run's wall clock as seen from X, +not X's own work. In run 41030496532, of 6911 cases carrying a duration +line, 6843 reported over 10 s, 6335 over 30 s and 1153 over 60 s, with 45 +under 1 s, against a run total of 131.530 s. A bound below the run's own +duration therefore does not measure the test at all, and whether it fires +depends on whether the timer enforcing it gets a thread — the same +starvation the 2026-09-19 update above measured on `DetachedProbe`'s +`DispatchQueue.global()` timer. + +So a `.timeLimit` has to sit ABOVE the whole suite's wall clock to be a +hang bound, and below the job's step budget to be worth having at all: +past the step budget the step is killed and the test's name is lost, which +is the `docs/BACKLOG.md` row "A test parked on a bare continuation +outlives its time limit". The 110 `.minutes(1)` bounds in `Tests/` were +raised to `.minutes(5)` on 2026-10-30 — 2.3x the 131.530 s run against +0.31x the 960 s step budget. + +Two limits of that fix, both unmeasured and left alone deliberately. It +couples the bound to the suite's size: a run twice as long makes five +minutes marginal again, so the figure above is a measurement with a shelf +life, not a constant. And the 27 `.minutes(2)` bounds were not touched — +no run has recorded one firing, though 120 s is 0.91x this run's total, +which makes them the next candidates rather than a cleared set. + Rule: assert the outcome and the ordering a bound is supposed to produce, not how long producing it took. A floor (`elapsed >= …`, "this did not return early") is fine — a slow machine cannot defeat it, only a -broken one. A ceiling always can. +broken one. A ceiling always can — including the one a `.timeLimit` +states, which is a ceiling on a clock no assertion chose, and which the +2026-10-30 update above measures firing on healthy tests. ## Architecture invariants diff --git a/Tests/macSCPAppKitTests/AlreadyOpenSessionTests.swift b/Tests/macSCPAppKitTests/AlreadyOpenSessionTests.swift index 07e2862f..f2ea881d 100644 --- a/Tests/macSCPAppKitTests/AlreadyOpenSessionTests.swift +++ b/Tests/macSCPAppKitTests/AlreadyOpenSessionTests.swift @@ -31,7 +31,7 @@ import Testing /// these tests leave in the strip is one of their own, built around /// `StartRecorder`, so a start that DOES go ahead is observed instead of /// dialed for real. -@Suite("Session already open", .timeLimit(.minutes(1))) +@Suite("Session already open", .timeLimit(.minutes(5))) @MainActor struct AlreadyOpenSessionTests { private func makeTempDirectory(_ label: String) -> URL { diff --git a/Tests/macSCPAppKitTests/ArchivePresentationTests.swift b/Tests/macSCPAppKitTests/ArchivePresentationTests.swift index 3fd76ce3..06edefd0 100644 --- a/Tests/macSCPAppKitTests/ArchivePresentationTests.swift +++ b/Tests/macSCPAppKitTests/ArchivePresentationTests.swift @@ -6,7 +6,7 @@ import Testing /// What the pane SAYS about an archive operation: one sentence per ending, /// one per refusal, and none of them built from a tool's output. -@Suite(.timeLimit(.minutes(1))) +@Suite(.timeLimit(.minutes(5))) struct ArchivePresentationTests { @Test func aCancelAndASuccessSayNothing() { #expect(ArchivePresentation.message(for: ArchiveActivity.Ending.finished) == nil) diff --git a/Tests/macSCPAppKitTests/BucketObjectLockSheetTests.swift b/Tests/macSCPAppKitTests/BucketObjectLockSheetTests.swift index cd3b061c..2f2a82e4 100644 --- a/Tests/macSCPAppKitTests/BucketObjectLockSheetTests.swift +++ b/Tests/macSCPAppKitTests/BucketObjectLockSheetTests.swift @@ -371,7 +371,7 @@ struct BucketObjectLockSheetTests { /// With a hang bound, for the reason given on the per-file sheet's twin of /// this test (`ObjectLockSheetTests.aSecondRunWhileOneIsInFlightSendsNothing`). - @MainActor @Test(.timeLimit(.minutes(1))) func aSecondSaveWhileOneIsInFlightSendsNothing() async { + @MainActor @Test(.timeLimit(.minutes(5))) func aSecondSaveWhileOneIsInFlightSendsNothing() async { let entered = AsyncSignal() let release = AsyncSignal() defer { release.signal() } diff --git a/Tests/macSCPAppKitTests/BucketVersioningSheetTests.swift b/Tests/macSCPAppKitTests/BucketVersioningSheetTests.swift index 90b9f921..cb6c291c 100644 --- a/Tests/macSCPAppKitTests/BucketVersioningSheetTests.swift +++ b/Tests/macSCPAppKitTests/BucketVersioningSheetTests.swift @@ -293,7 +293,7 @@ struct BucketVersioningSheetTests { /// while the versioning read has already returned; in that window the sheet /// must still be `.reading`, not a settled sheet with no button. Only when /// the lock answer is in does the load settle, with both. - @MainActor @Test(.timeLimit(.minutes(1))) + @MainActor @Test(.timeLimit(.minutes(5))) func theVersioningAnswerDoesNotSettleBeforeTheLockAnswer() async { let entered = AsyncSignal() let release = AsyncSignal() diff --git a/Tests/macSCPAppKitTests/ComparisonFetchTests.swift b/Tests/macSCPAppKitTests/ComparisonFetchTests.swift index d8396151..828b5b65 100644 --- a/Tests/macSCPAppKitTests/ComparisonFetchTests.swift +++ b/Tests/macSCPAppKitTests/ComparisonFetchTests.swift @@ -639,7 +639,7 @@ struct ComparisonFetchTests { /// an arrow key. Built from a real queue and a real cancel; every /// postcondition is read BEFORE the parked read is let go, because the /// read ending would make the queue look tidy whatever the cancel did. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func aDiffWhoseTaskIsCancelledMidFetchLeavesNoTransferRunning() async throws { let root = try makeTemporaryRoot() defer { try? FileManager.default.removeItem(at: root) } @@ -725,7 +725,7 @@ struct ComparisonFetchTests { /// /// Every postcondition is read BEFORE the parked read is let go: the read /// ending would make the queue tidy whatever the cancel did. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func aSettleInFlightIsCancelledByTheComparisonsOwnCancel() async throws { let root = try makeTemporaryRoot() defer { try? FileManager.default.removeItem(at: root) } @@ -784,7 +784,7 @@ struct ComparisonFetchTests { /// started the same way and NOT cancelled, settles the row and reads both /// sides. Without this, a `startTrackedWork` that cancelled every task it /// was handed would pass the test above. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func aSettleNobodyCancelsSettlesTheRowAndReadsBothSides() async throws { let root = try makeTemporaryRoot() defer { try? FileManager.default.removeItem(at: root) } @@ -812,7 +812,7 @@ struct ComparisonFetchTests { /// cannot be satisfied by the other. Here the left file arrives and /// finishes, the right read is parked, and the task is cancelled — which /// is what selecting a row, waiting a moment and arrowing on does. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func aDiffWhoseTaskIsCancelledDuringTheRightDownloadLeavesNoTransferRunning() async throws { let root = try makeTemporaryRoot() defer { try? FileManager.default.removeItem(at: root) } @@ -849,7 +849,7 @@ struct ComparisonFetchTests { } /// The click route to the same fetch. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func aSettleWhoseTaskIsCancelledMidFetchLeavesNoTransferRunning() async throws { let root = try makeTemporaryRoot() defer { try? FileManager.default.removeItem(at: root) } diff --git a/Tests/macSCPAppKitTests/ComparisonOverviewTests.swift b/Tests/macSCPAppKitTests/ComparisonOverviewTests.swift index 9572b3bc..59446d52 100644 --- a/Tests/macSCPAppKitTests/ComparisonOverviewTests.swift +++ b/Tests/macSCPAppKitTests/ComparisonOverviewTests.swift @@ -24,7 +24,7 @@ import macSCPCore /// `en.lproj` separately, by reading the file. /// /// No test asserts how long anything took. -@Suite("Comparison overview", .timeLimit(.minutes(1))) +@Suite("Comparison overview", .timeLimit(.minutes(5))) @MainActor struct ComparisonOverviewTests { private typealias Presentation = ComparisonOverviewPresentation diff --git a/Tests/macSCPAppKitTests/ComparisonOwnershipTests.swift b/Tests/macSCPAppKitTests/ComparisonOwnershipTests.swift index 722f2fa3..0d1c10ff 100644 --- a/Tests/macSCPAppKitTests/ComparisonOwnershipTests.swift +++ b/Tests/macSCPAppKitTests/ComparisonOwnershipTests.swift @@ -25,7 +25,7 @@ import Testing /// No test here asserts how long anything took. A walk parked on a peer is /// modelled by a call that waits until it is cancelled; the suite's hang /// bound is `.timeLimit`. -@Suite("Comparison ownership", .timeLimit(.minutes(1))) +@Suite("Comparison ownership", .timeLimit(.minutes(5))) @MainActor struct ComparisonOwnershipTests { diff --git a/Tests/macSCPAppKitTests/ComparisonWindowModelTests.swift b/Tests/macSCPAppKitTests/ComparisonWindowModelTests.swift index 95a70655..74d4d61a 100644 --- a/Tests/macSCPAppKitTests/ComparisonWindowModelTests.swift +++ b/Tests/macSCPAppKitTests/ComparisonWindowModelTests.swift @@ -22,7 +22,7 @@ import Testing /// Both sides are the real local file system over temporary directories, so /// a walk finishes without a network. No test asserts how long anything took; /// the suite's hang bound is `.timeLimit`. -@Suite("Comparison window model", .timeLimit(.minutes(1))) +@Suite("Comparison window model", .timeLimit(.minutes(5))) @MainActor struct ComparisonWindowModelTests { diff --git a/Tests/macSCPAppKitTests/ConnectAttemptHandoffTests.swift b/Tests/macSCPAppKitTests/ConnectAttemptHandoffTests.swift index 1ea37947..608945f8 100644 --- a/Tests/macSCPAppKitTests/ConnectAttemptHandoffTests.swift +++ b/Tests/macSCPAppKitTests/ConnectAttemptHandoffTests.swift @@ -63,7 +63,7 @@ import Testing /// from the byte comparison — parsed via `SessionStore`'s own real reader, /// which is a READ, not a write, and therefore safe regardless of what a /// broken seam did. -@Suite("Connect attempt hand-off", .timeLimit(.minutes(1))) +@Suite("Connect attempt hand-off", .timeLimit(.minutes(5))) @MainActor struct ConnectAttemptHandoffTests { /// The ONE real, on-disk file this whole suite must never write — diff --git a/Tests/macSCPAppKitTests/DetailSurfacePlanTests.swift b/Tests/macSCPAppKitTests/DetailSurfacePlanTests.swift index 2325266b..a6181633 100644 --- a/Tests/macSCPAppKitTests/DetailSurfacePlanTests.swift +++ b/Tests/macSCPAppKitTests/DetailSurfacePlanTests.swift @@ -26,7 +26,7 @@ import Testing /// raises the text is `FormFailureAlertPlanTests`' claim; that the view /// switches on this answer and on nothing else is /// `DetailSurfaceWiringGuardTests`'. -@Suite("Detail surface plan", .timeLimit(.minutes(1))) +@Suite("Detail surface plan", .timeLimit(.minutes(5))) @MainActor struct DetailSurfacePlanTests { private static let selected = StoredSession( diff --git a/Tests/macSCPAppKitTests/ErrorNotificationTests.swift b/Tests/macSCPAppKitTests/ErrorNotificationTests.swift index b8a4cc17..db6de6cb 100644 --- a/Tests/macSCPAppKitTests/ErrorNotificationTests.swift +++ b/Tests/macSCPAppKitTests/ErrorNotificationTests.swift @@ -16,7 +16,7 @@ import UserNotifications /// are driven through the real functions that own them /// (`ContentView.handleLivenessGiveUp(_:)`, /// `ContentView.notifyTransferFailures()`, `TunnelManager`'s state mirror). -@Suite("Error notifications", .timeLimit(.minutes(1))) +@Suite("Error notifications", .timeLimit(.minutes(5))) @MainActor struct ErrorNotificationTests { diff --git a/Tests/macSCPAppKitTests/HostKeyMismatchHintGuardTests.swift b/Tests/macSCPAppKitTests/HostKeyMismatchHintGuardTests.swift index e497e7a8..adb306db 100644 --- a/Tests/macSCPAppKitTests/HostKeyMismatchHintGuardTests.swift +++ b/Tests/macSCPAppKitTests/HostKeyMismatchHintGuardTests.swift @@ -13,7 +13,7 @@ import Testing /// The negative direction (the message contains the label) is paired with /// positive checks that the label is non-empty and that the message is longer /// than the label, so the test cannot pass by matching an empty string. -@Suite("Host-key mismatch hint names real menu items", .timeLimit(.minutes(1))) +@Suite("Host-key mismatch hint names real menu items", .timeLimit(.minutes(5))) struct HostKeyMismatchHintGuardTests { private static let repoRoot = URL(fileURLWithPath: #filePath) .deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() diff --git a/Tests/macSCPAppKitTests/HostResolveModelTests.swift b/Tests/macSCPAppKitTests/HostResolveModelTests.swift index fea82171..98c7b071 100644 --- a/Tests/macSCPAppKitTests/HostResolveModelTests.swift +++ b/Tests/macSCPAppKitTests/HostResolveModelTests.swift @@ -22,7 +22,7 @@ import Testing /// that nothing on this path writes a known host, is /// `HostResolveWiringGuardTests`' claim, read from source. @MainActor -@Suite("Resolving the host field", .timeLimit(.minutes(1))) +@Suite("Resolving the host field", .timeLimit(.minutes(5))) struct HostResolveModelTests { private static func makeForm() -> ConnectionViewModel { ConnectionViewModel(connector: { _, _ in diff --git a/Tests/macSCPAppKitTests/JumpTwoTabsTests.swift b/Tests/macSCPAppKitTests/JumpTwoTabsTests.swift index 14bbc79a..71f5eb92 100644 --- a/Tests/macSCPAppKitTests/JumpTwoTabsTests.swift +++ b/Tests/macSCPAppKitTests/JumpTwoTabsTests.swift @@ -55,7 +55,7 @@ import Testing /// That case makes a real loopback TCP dial on purpose, and the port is /// asked for rather than spelled so that no listener of the machine's can /// change its verdict. -@Suite("Two tabs through one jump", .timeLimit(.minutes(1))) +@Suite("Two tabs through one jump", .timeLimit(.minutes(5))) @MainActor struct JumpTwoTabsTests { // MARK: - Fixtures diff --git a/Tests/macSCPAppKitTests/KeyPassphraseActionsTests.swift b/Tests/macSCPAppKitTests/KeyPassphraseActionsTests.swift index f890873b..cd22ed06 100644 --- a/Tests/macSCPAppKitTests/KeyPassphraseActionsTests.swift +++ b/Tests/macSCPAppKitTests/KeyPassphraseActionsTests.swift @@ -24,7 +24,7 @@ import Testing /// members of a `View` and inherit its isolation; calling them from a /// nonisolated suite compiles with an `#ActorIsolatedCall` warning, and the /// warning budget here is zero. -@Suite("Managed key passphrase actions", .timeLimit(.minutes(1))) +@Suite("Managed key passphrase actions", .timeLimit(.minutes(5))) @MainActor struct KeyPassphraseActionsTests { private static let sourceFile = SourceCorpus.url(of: .sources) diff --git a/Tests/macSCPAppKitTests/LivenessProbeCancellationTests.swift b/Tests/macSCPAppKitTests/LivenessProbeCancellationTests.swift index 7a73cb48..fa0b1a39 100644 --- a/Tests/macSCPAppKitTests/LivenessProbeCancellationTests.swift +++ b/Tests/macSCPAppKitTests/LivenessProbeCancellationTests.swift @@ -34,7 +34,7 @@ import Testing /// file system this suite hands its sessions never answers and never touches /// disk, and the deadline is the shortest `probeTimeout(forInterval:)` can /// produce. -@Suite("Liveness probe cancellation", .timeLimit(.minutes(1))) +@Suite("Liveness probe cancellation", .timeLimit(.minutes(5))) @MainActor struct LivenessProbeCancellationTests { /// The shortest deadline the shipped policy can produce diff --git a/Tests/macSCPAppKitTests/LivenessProbeCauseTests.swift b/Tests/macSCPAppKitTests/LivenessProbeCauseTests.swift index 7e9cfc9f..eaa047e2 100644 --- a/Tests/macSCPAppKitTests/LivenessProbeCauseTests.swift +++ b/Tests/macSCPAppKitTests/LivenessProbeCauseTests.swift @@ -11,7 +11,7 @@ import Testing /// /// Isolation as in `LivenessProbeCancellationTests`: no `ContentView`, no /// store, no network. -@Suite("Liveness probe cause", .timeLimit(.minutes(1))) +@Suite("Liveness probe cause", .timeLimit(.minutes(5))) @MainActor struct LivenessProbeCauseTests { @Test func aThrownConnectionLossIsRecordedAsAClosedConnection() async { @@ -166,7 +166,7 @@ private final class ScriptedStatFileSystem: RemoteFileSystem, @unchecked Sendabl /// place every disconnect goes through, and it is where the App stops /// listening — so a close arriving afterwards cannot write a line naming a /// tab whose session is gone. -@Suite("Close report ends at teardown", .timeLimit(.minutes(1))) +@Suite("Close report ends at teardown", .timeLimit(.minutes(5))) @MainActor struct CloseReportTeardownTests { @Test func teardownStopsTheCloseReport() async { diff --git a/Tests/macSCPAppKitTests/LivenessProbeRaceTests.swift b/Tests/macSCPAppKitTests/LivenessProbeRaceTests.swift index 2e5865d5..783d10bb 100644 --- a/Tests/macSCPAppKitTests/LivenessProbeRaceTests.swift +++ b/Tests/macSCPAppKitTests/LivenessProbeRaceTests.swift @@ -90,7 +90,7 @@ struct LivenessProbeRaceTests { // whole suite, not this race. Raising the number would only move the // hostage line; `.timeLimit` states the actual claim — this must not // hang — and lets the harness enforce it independently of contention. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func aStatThatNeverRespondsStillReportsFailureWithinTheDeadline() async { let fs = NeverRespondingFileSystem() let start = ContinuousClock.now diff --git a/Tests/macSCPAppKitTests/ObjectLockSheetTests.swift b/Tests/macSCPAppKitTests/ObjectLockSheetTests.swift index 2c9d2669..f68ce1b0 100644 --- a/Tests/macSCPAppKitTests/ObjectLockSheetTests.swift +++ b/Tests/macSCPAppKitTests/ObjectLockSheetTests.swift @@ -733,7 +733,7 @@ struct ObjectLockSheetTests { /// 2026-10-21 by planting out `guard canRun` in `run`, which left the run /// parked for ten minutes with no verdict. The limit turns that into a red; /// the `defer` lets go of anything still held when the test ends either way. - @MainActor @Test(.timeLimit(.minutes(1))) func aSecondRunWhileOneIsInFlightSendsNothing() async { + @MainActor @Test(.timeLimit(.minutes(5))) func aSecondRunWhileOneIsInFlightSendsNothing() async { let entered = AsyncSignal() let release = AsyncSignal() defer { release.signal() } diff --git a/Tests/macSCPAppKitTests/QuitSequenceTests.swift b/Tests/macSCPAppKitTests/QuitSequenceTests.swift index 225d88b6..b24e32c5 100644 --- a/Tests/macSCPAppKitTests/QuitSequenceTests.swift +++ b/Tests/macSCPAppKitTests/QuitSequenceTests.swift @@ -23,7 +23,8 @@ import Testing /// wiring guards read a view body. Every positional pin below is also a /// presence check: a needle that has moved out of the body fails on "not /// found" before any ordering claim is made. -/// `.timeLimit(.minutes(1))` since fix round 2 of the port-forwarding +/// `.timeLimit(.minutes(5))` — raised from one minute on 2026-10-30, and a +/// bound here at all since fix round 2 of the port-forwarding /// plan's Task 6: this suite now drives `BoundedStep` against work that /// parks and cannot be cancelled, and it reads the outcome through a /// `pollUntil` — which carries no deadline of its own, deliberately, so what @@ -31,7 +32,7 @@ import Testing /// .everyCallerOfPollUntilDeclaresATimeLimit` requires it, and without it a /// version that does not bound HANGS the run instead of failing it — /// measured, by planting exactly that). -@Suite("Quit sequence", .timeLimit(.minutes(1))) +@Suite("Quit sequence", .timeLimit(.minutes(5))) @MainActor struct QuitSequenceTests { diff --git a/Tests/macSCPAppKitTests/ReconnectPathTests.swift b/Tests/macSCPAppKitTests/ReconnectPathTests.swift index 7d79f16b..1d5e0aa7 100644 --- a/Tests/macSCPAppKitTests/ReconnectPathTests.swift +++ b/Tests/macSCPAppKitTests/ReconnectPathTests.swift @@ -22,7 +22,7 @@ import Testing /// see that file's own "Isolation, round 3" — and /// `theRealSessionsFileIsNeverTouched` proves the isolation empirically /// rather than by reading the code. -@Suite("Reconnect path", .timeLimit(.minutes(1))) +@Suite("Reconnect path", .timeLimit(.minutes(5))) @MainActor struct ReconnectPathTests { private static var realSessionsFileURL: URL { diff --git a/Tests/macSCPAppKitTests/SSHTerminalViewSizingTests.swift b/Tests/macSCPAppKitTests/SSHTerminalViewSizingTests.swift index e1293deb..aa45473d 100644 --- a/Tests/macSCPAppKitTests/SSHTerminalViewSizingTests.swift +++ b/Tests/macSCPAppKitTests/SSHTerminalViewSizingTests.swift @@ -23,7 +23,7 @@ import macSCPCore /// /// The three tests below take the three hops apart, so a red run names the /// hop rather than the symptom. -@Suite("Terminal resize measurement", .serialized, .timeLimit(.minutes(1))) +@Suite("Terminal resize measurement", .serialized, .timeLimit(.minutes(5))) @MainActor struct SSHTerminalViewSizingTests { diff --git a/Tests/macSCPAppKitTests/SnippetAfterConnectSequenceTests.swift b/Tests/macSCPAppKitTests/SnippetAfterConnectSequenceTests.swift index 66bde2fc..4801eb66 100644 --- a/Tests/macSCPAppKitTests/SnippetAfterConnectSequenceTests.swift +++ b/Tests/macSCPAppKitTests/SnippetAfterConnectSequenceTests.swift @@ -35,7 +35,7 @@ import Testing /// the maintainer's real session store and real Keychain. The tab the window /// starts with is replaced by one of this suite's own, so the only connector /// reachable from `tabsModel` is the fake. -@Suite("Snippet after connect", .timeLimit(.minutes(1))) +@Suite("Snippet after connect", .timeLimit(.minutes(5))) @MainActor struct SnippetAfterConnectSequenceTests { // MARK: - Harness diff --git a/Tests/macSCPAppKitTests/StoreReloadOnActivationGuardTests.swift b/Tests/macSCPAppKitTests/StoreReloadOnActivationGuardTests.swift index 540d4ea1..887f5db1 100644 --- a/Tests/macSCPAppKitTests/StoreReloadOnActivationGuardTests.swift +++ b/Tests/macSCPAppKitTests/StoreReloadOnActivationGuardTests.swift @@ -477,7 +477,7 @@ struct StoreReloadOnActivationGuardTests { /// `TabRegistry()` instances of their own, never `.shared` — the rule that /// type's own doc comment states, so a registration here can never leak into /// another test. -@Suite("Every open window's session list", .timeLimit(.minutes(1))) +@Suite("Every open window's session list", .timeLimit(.minutes(5))) @MainActor struct SessionListRegistrationTests { diff --git a/Tests/macSCPAppKitTests/TabRegistryTests.swift b/Tests/macSCPAppKitTests/TabRegistryTests.swift index 92813cc8..fab5d8e9 100644 --- a/Tests/macSCPAppKitTests/TabRegistryTests.swift +++ b/Tests/macSCPAppKitTests/TabRegistryTests.swift @@ -321,7 +321,7 @@ private final class UnreachedSecretStore: SecretStore, @unchecked Sendable { /// /// `TabRegistry()` instances of their own, never `.shared` — the rule that /// type's own doc comment states. -@Suite("A tab's terminal type follows the tab", .timeLimit(.minutes(1))) +@Suite("A tab's terminal type follows the tab", .timeLimit(.minutes(5))) @MainActor struct TerminalTypeWindowScopeTests { diff --git a/Tests/macSCPAppKitTests/TabTitlePlanTests.swift b/Tests/macSCPAppKitTests/TabTitlePlanTests.swift index 7b2c5c87..d15e3a39 100644 --- a/Tests/macSCPAppKitTests/TabTitlePlanTests.swift +++ b/Tests/macSCPAppKitTests/TabTitlePlanTests.swift @@ -13,7 +13,7 @@ import Testing /// describing a session said "New Connection". That the strip and the /// window title draw this plan's answer is `TabTitleWiringGuardTests`' /// claim, not this suite's. -@Suite("Tab title plan", .timeLimit(.minutes(1))) +@Suite("Tab title plan", .timeLimit(.minutes(5))) @MainActor struct TabTitlePlanTests { private static let target = StoredSession( diff --git a/Tests/macSCPAppKitTests/TeardownStageTests.swift b/Tests/macSCPAppKitTests/TeardownStageTests.swift index 49c1a262..89bd94f8 100644 --- a/Tests/macSCPAppKitTests/TeardownStageTests.swift +++ b/Tests/macSCPAppKitTests/TeardownStageTests.swift @@ -50,7 +50,7 @@ struct TeardownStageTests { func raise() { lock.withLock { raised = true } } } - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func aStageThatNeverReturnsIsAbandoned() async { // `boundSeconds: 0` rather than the stage's own five: this asserts // the abandonment BRANCH, and spending the real bound to reach it @@ -79,7 +79,7 @@ struct TeardownStageTests { /// /// A bound forwarded as zero is caught by `finished` itself rather than /// by a clock: the bound would win the race and this would be `false`. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func aStageThatReturnsIsNotAbandonedAndDoesNotWaitOutItsBound() async { let finished = await TeardownStage.stopEditWatchers .runBounded(boundSeconds: 3600) {} @@ -89,7 +89,7 @@ struct TeardownStageTests { /// The property the whole per-stage design exists for, and the one a /// single bound around all of `teardown` would not have: giving up on /// one stage costs that stage only. Teardown runs to its end. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func abandoningOneStageStillLetsTheNextOneRun() async { let ran = Flag() let abandoned = await TeardownStage.stopEditWatchers diff --git a/Tests/macSCPAppKitTests/TunnelDockPresenceTests.swift b/Tests/macSCPAppKitTests/TunnelDockPresenceTests.swift index ee411581..9865dd6c 100644 --- a/Tests/macSCPAppKitTests/TunnelDockPresenceTests.swift +++ b/Tests/macSCPAppKitTests/TunnelDockPresenceTests.swift @@ -21,7 +21,7 @@ import Testing /// The manager runs on `TunnelManagerTests.Rig` — the same fake runners, the /// same real `TunnelStore` on a temporary directory — reused rather than /// copied. -@Suite("Tunnel Dock presence", .timeLimit(.minutes(1))) +@Suite("Tunnel Dock presence", .timeLimit(.minutes(5))) @MainActor struct TunnelDockPresenceTests { diff --git a/Tests/macSCPAppKitTests/TunnelHostKeyPromptBridgeTests.swift b/Tests/macSCPAppKitTests/TunnelHostKeyPromptBridgeTests.swift index c2736194..b51c0f4b 100644 --- a/Tests/macSCPAppKitTests/TunnelHostKeyPromptBridgeTests.swift +++ b/Tests/macSCPAppKitTests/TunnelHostKeyPromptBridgeTests.swift @@ -28,7 +28,7 @@ import Testing /// of these tests hang until the probe was killed by hand, with no verdict /// at all. Polling for the recorded answer is cancellable, so the same plant /// is red instead. -@Suite("Tunnel host-key prompt bridge", .timeLimit(.minutes(1))) +@Suite("Tunnel host-key prompt bridge", .timeLimit(.minutes(5))) @MainActor struct TunnelHostKeyPromptBridgeTests { diff --git a/Tests/macSCPAppKitTests/TunnelManagerTests.swift b/Tests/macSCPAppKitTests/TunnelManagerTests.swift index ccc8a3b7..a2b28876 100644 --- a/Tests/macSCPAppKitTests/TunnelManagerTests.swift +++ b/Tests/macSCPAppKitTests/TunnelManagerTests.swift @@ -18,7 +18,7 @@ import Testing /// The store is a real `TunnelStore` on a temporary directory: the profiles /// the manager hands out come back through JSON, so a save that does not /// persist fails here rather than in a dev build. -@Suite("Tunnel manager", .timeLimit(.minutes(1))) +@Suite("Tunnel manager", .timeLimit(.minutes(5))) @MainActor struct TunnelManagerTests { diff --git a/Tests/macSCPCoreTests/AgentAuthTests.swift b/Tests/macSCPCoreTests/AgentAuthTests.swift index e2ed01bd..e5d567a9 100644 --- a/Tests/macSCPCoreTests/AgentAuthTests.swift +++ b/Tests/macSCPCoreTests/AgentAuthTests.swift @@ -31,11 +31,18 @@ import Testing /// (restored in each test) — serializing avoids any cross-test interleaving /// on that shared, process-global state. /// -/// `.timeLimit(.minutes(1))`: nothing here generates a key or touches a +/// `.timeLimit(.minutes(5))`: nothing here generates a key or touches a /// real network — the mock transport is the slowest thing any test drives, /// and `signTimesOutWithProtocolError` bounds its own wait with a 0.05 s -/// `signTimeout` regardless. The project default is enough. -@Suite("AgentAuth", .serialized, .timeLimit(.minutes(1))) +/// `signTimeout` regardless. That is an argument for not needing a LONG +/// bound and it still holds; it was written as "The project default is +/// enough" against a default of one minute — the withdrawn wording. One +/// minute was not above what an awaiting case here reports on a three-core +/// runner, because what it reports is the whole run's wall clock rather +/// than its own work (CLAUDE.md, "A wall-clock ceiling in a test measures +/// the runner", 2026-10-30). Five minutes now, like the rest of the tree, +/// and nothing in this suite is expected to approach it. +@Suite("AgentAuth", .serialized, .timeLimit(.minutes(5))) struct AgentAuthTests { // MARK: - Fixtures diff --git a/Tests/macSCPCoreTests/AgentEnvLockTests.swift b/Tests/macSCPCoreTests/AgentEnvLockTests.swift index 555ba97e..7e10fcc6 100644 --- a/Tests/macSCPCoreTests/AgentEnvLockTests.swift +++ b/Tests/macSCPCoreTests/AgentEnvLockTests.swift @@ -17,7 +17,7 @@ import Testing /// suites use concurrently for their own, unrelated `SSH_AUTH_SOCK` /// critical sections — sharing it here would risk this test interfering /// with (or being interfered with by) them. -@Suite("AgentEnvLock", .timeLimit(.minutes(1))) +@Suite("AgentEnvLock", .timeLimit(.minutes(5))) struct AgentEnvLockTests { /// The scenario the bug produced: A holds the lock, B queues behind /// it, B is cancelled while still queued, A releases — and a fresh C diff --git a/Tests/macSCPCoreTests/ArchiveActivityTests.swift b/Tests/macSCPCoreTests/ArchiveActivityTests.swift index 16e45936..3be843bd 100644 --- a/Tests/macSCPCoreTests/ArchiveActivityTests.swift +++ b/Tests/macSCPCoreTests/ArchiveActivityTests.swift @@ -6,7 +6,7 @@ import Foundation /// the pane reads it from SwiftUI. Nothing in here blocks the main actor -- /// every wait is an `await` on a continuation. @MainActor -@Suite(.timeLimit(.minutes(1))) +@Suite(.timeLimit(.minutes(5))) struct ArchiveActivityTests { /// A runner that parks until the test releases it, so the running state /// can be read WHILE it is running. Parked on `AsyncSignal`, awaited -- diff --git a/Tests/macSCPCoreTests/ArchiveCommandChannelTests.swift b/Tests/macSCPCoreTests/ArchiveCommandChannelTests.swift index b51ef1b0..01f8aedc 100644 --- a/Tests/macSCPCoreTests/ArchiveCommandChannelTests.swift +++ b/Tests/macSCPCoreTests/ArchiveCommandChannelTests.swift @@ -47,7 +47,7 @@ final actor FailingArchiveChannel: ArchiveCommandChannel { } } -@Suite(.timeLimit(.minutes(1))) +@Suite(.timeLimit(.minutes(5))) struct ArchiveCommandChannelTests { @Test func aChannelIsHandedTheLineAndTheBytesSeparately() async throws { let channel = RecordingArchiveChannel() diff --git a/Tests/macSCPCoreTests/ArchiveCommandLineTests.swift b/Tests/macSCPCoreTests/ArchiveCommandLineTests.swift index 872a1b92..6d903677 100644 --- a/Tests/macSCPCoreTests/ArchiveCommandLineTests.swift +++ b/Tests/macSCPCoreTests/ArchiveCommandLineTests.swift @@ -2,7 +2,7 @@ import Testing import Foundation @testable import macSCPCore -@Suite(.timeLimit(.minutes(1))) +@Suite(.timeLimit(.minutes(5))) struct ArchiveCommandLineTests { private func file(_ name: String) -> RemoteFileItem { RemoteFileItem(name: name, path: "/d/" + name, kind: .file) diff --git a/Tests/macSCPCoreTests/ArchiveFormatTests.swift b/Tests/macSCPCoreTests/ArchiveFormatTests.swift index aff68388..95fa0638 100644 --- a/Tests/macSCPCoreTests/ArchiveFormatTests.swift +++ b/Tests/macSCPCoreTests/ArchiveFormatTests.swift @@ -2,7 +2,7 @@ import Testing import Foundation @testable import macSCPCore -@Suite(.timeLimit(.minutes(1))) +@Suite(.timeLimit(.minutes(5))) struct ArchiveFormatTests { private func file(_ name: String) -> RemoteFileItem { RemoteFileItem(name: name, path: "/d/" + name, kind: .file) diff --git a/Tests/macSCPCoreTests/ArchiveMenuEntriesTests.swift b/Tests/macSCPCoreTests/ArchiveMenuEntriesTests.swift index e8ab292b..48f44db4 100644 --- a/Tests/macSCPCoreTests/ArchiveMenuEntriesTests.swift +++ b/Tests/macSCPCoreTests/ArchiveMenuEntriesTests.swift @@ -1,7 +1,7 @@ import Testing @testable import macSCPCore -@Suite(.timeLimit(.minutes(1))) +@Suite(.timeLimit(.minutes(5))) struct ArchiveMenuEntriesTests { private func file(_ name: String) -> RemoteFileItem { RemoteFileItem(name: name, path: "/d/" + name, kind: .file) diff --git a/Tests/macSCPCoreTests/ArchivePlanTests.swift b/Tests/macSCPCoreTests/ArchivePlanTests.swift index 4898ef2f..679ffdb5 100644 --- a/Tests/macSCPCoreTests/ArchivePlanTests.swift +++ b/Tests/macSCPCoreTests/ArchivePlanTests.swift @@ -2,7 +2,7 @@ import Testing import Foundation @testable import macSCPCore -@Suite(.timeLimit(.minutes(1))) +@Suite(.timeLimit(.minutes(5))) struct ArchivePlanTests { private func file(_ name: String) -> RemoteFileItem { RemoteFileItem(name: name, path: "/d/" + name, kind: .file) diff --git a/Tests/macSCPCoreTests/AsyncSignalTests.swift b/Tests/macSCPCoreTests/AsyncSignalTests.swift index 846ee33b..50a019c7 100644 --- a/Tests/macSCPCoreTests/AsyncSignalTests.swift +++ b/Tests/macSCPCoreTests/AsyncSignalTests.swift @@ -11,7 +11,7 @@ import Testing /// under cancellation the `Task.sleep` branch of `race` throws at once and /// wins the race before the collapsed `wait()` can answer. A property that /// another path happens to cover today is not a property that is guarded. -@Suite("AsyncSignal", .timeLimit(.minutes(1))) +@Suite("AsyncSignal", .timeLimit(.minutes(5))) struct AsyncSignalTests { /// The positive half. Without it the cancellation test below would go on /// passing against a `wait()` that answers `.cancelled` for everything. diff --git a/Tests/macSCPCoreTests/AwaitResumptionTests.swift b/Tests/macSCPCoreTests/AwaitResumptionTests.swift index 941bb1fb..3297cc6b 100644 --- a/Tests/macSCPCoreTests/AwaitResumptionTests.swift +++ b/Tests/macSCPCoreTests/AwaitResumptionTests.swift @@ -13,7 +13,7 @@ import MacSCPTestSupport /// no clock of any kind, per CLAUDE.md "A wall-clock ceiling in a test /// measures the runner": the only thing that ends a wait here is either the /// body resuming, or `.timeLimit` cancelling the suite. -@Suite("awaitResumption", .timeLimit(.minutes(1))) +@Suite("awaitResumption", .timeLimit(.minutes(5))) struct AwaitResumptionTests { private enum ProbeError: Error, Equatable { case thrown } diff --git a/Tests/macSCPCoreTests/BoundedCloseTests.swift b/Tests/macSCPCoreTests/BoundedCloseTests.swift index 573ad812..5e874813 100644 --- a/Tests/macSCPCoreTests/BoundedCloseTests.swift +++ b/Tests/macSCPCoreTests/BoundedCloseTests.swift @@ -42,7 +42,7 @@ struct BoundedCloseTests { /// contention as much as it measures this race, and a number large /// enough to be safe is too large to mean anything. "This must not /// hang" is the actual claim. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func anOperationThatNeverReturnsIsAbandonedAtTheBound() async { let startedAt = ContinuousClock.now let finished = await BoundedClose.run(boundSeconds: 1) { await self.neverReturns() } @@ -72,7 +72,7 @@ struct BoundedCloseTests { /// stall `ConnectMainActorLivenessTests` records at up to 14.67 s — so /// the ceiling was measuring the runner, exactly as CLAUDE.md's "A /// wall-clock ceiling in a test measures the runner" says. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func anOperationThatFinishesInsideTheBoundIsNotAbandoned() async { let finished = await BoundedClose.run(boundSeconds: 120) {} #expect(finished == true) diff --git a/Tests/macSCPCoreTests/BytePumpTests.swift b/Tests/macSCPCoreTests/BytePumpTests.swift index 84f5c58c..ee88cf30 100644 --- a/Tests/macSCPCoreTests/BytePumpTests.swift +++ b/Tests/macSCPCoreTests/BytePumpTests.swift @@ -24,7 +24,7 @@ import Testing /// gets its own test, and that test asserts the two channels really are /// on different loops before it asserts anything else — without that a /// same-loop run would pass while proving nothing. -@Suite("BytePump", .timeLimit(.minutes(1))) +@Suite("BytePump", .timeLimit(.minutes(5))) struct BytePumpTests { // MARK: - Same loop: EmbeddedChannel pairs diff --git a/Tests/macSCPCoreTests/CLIDiagnoseForegroundRunTests.swift b/Tests/macSCPCoreTests/CLIDiagnoseForegroundRunTests.swift index c6d93d2c..7fc8b747 100644 --- a/Tests/macSCPCoreTests/CLIDiagnoseForegroundRunTests.swift +++ b/Tests/macSCPCoreTests/CLIDiagnoseForegroundRunTests.swift @@ -12,7 +12,7 @@ import Testing /// The fake diagnoses park on an `AsyncSignal` — until they are cancelled, /// or until the case releases them — and never on a clock. The time limit /// is a hang bound. -@Suite("CLI diagnose foreground run", .timeLimit(.minutes(1))) +@Suite("CLI diagnose foreground run", .timeLimit(.minutes(5))) struct CLIDiagnoseForegroundRunTests { /// The first signal cancels the diagnosis and WAITS for it: the walk's /// cleanup — the throughput test's removal — runs, and its report is diff --git a/Tests/macSCPCoreTests/CLIMatrixITests.swift b/Tests/macSCPCoreTests/CLIMatrixITests.swift index 04dbe66a..36826a6b 100644 --- a/Tests/macSCPCoreTests/CLIMatrixITests.swift +++ b/Tests/macSCPCoreTests/CLIMatrixITests.swift @@ -3001,7 +3001,7 @@ struct CLIMatrixCoverageTests { /// cancellation, so a generous limit would hide the very regression it /// exists for. The limit is still a net rather than a measurement — nothing /// below asserts on elapsed time. -@Suite("CLIMatrixRunUntilLine", .timeLimit(.minutes(1))) +@Suite("CLIMatrixRunUntilLine", .timeLimit(.minutes(5))) struct CLIMatrixRunUntilLineTests { private static let shell = URL(fileURLWithPath: "/bin/sh") diff --git a/Tests/macSCPCoreTests/CLISecretSourcesTests.swift b/Tests/macSCPCoreTests/CLISecretSourcesTests.swift index 0e29ed05..06cc2a18 100644 --- a/Tests/macSCPCoreTests/CLISecretSourcesTests.swift +++ b/Tests/macSCPCoreTests/CLISecretSourcesTests.swift @@ -2,7 +2,7 @@ import Foundation import Testing @testable import macSCPCore -@Suite("PasswordCommandSecretSource", .timeLimit(.minutes(1))) +@Suite("PasswordCommandSecretSource", .timeLimit(.minutes(5))) struct PasswordCommandSecretSourceTests { @Test func returnsTheCommandsOutputWithTrailingNewlineStripped() throws { let source = PasswordCommandSecretSource(command: "echo hunter2") diff --git a/Tests/macSCPCoreTests/CLITunnelForegroundRunTests.swift b/Tests/macSCPCoreTests/CLITunnelForegroundRunTests.swift index 311b2533..961e9a4b 100644 --- a/Tests/macSCPCoreTests/CLITunnelForegroundRunTests.swift +++ b/Tests/macSCPCoreTests/CLITunnelForegroundRunTests.swift @@ -17,7 +17,7 @@ import Testing /// /// Every wait is an `await` on `pollUntil` (CLAUDE.md, "Tests never block /// the cooperative pool"), and nothing here asserts an elapsed time. -@Suite("CLI tunnels start foreground loop", .timeLimit(.minutes(1))) +@Suite("CLI tunnels start foreground loop", .timeLimit(.minutes(5))) struct CLITunnelForegroundRunTests { // MARK: - The clean sequence diff --git a/Tests/macSCPCoreTests/ConnectFailureSecrecyTests.swift b/Tests/macSCPCoreTests/ConnectFailureSecrecyTests.swift index 530ad03b..9e28bec7 100644 --- a/Tests/macSCPCoreTests/ConnectFailureSecrecyTests.swift +++ b/Tests/macSCPCoreTests/ConnectFailureSecrecyTests.swift @@ -22,7 +22,7 @@ import Testing /// `jumpAwareFailedState` and adds nothing to the text. Driving `connect()` /// itself would additionally require a resolved form, and resolving a form /// means a secret store; these tests deliberately touch none. -@Suite("Connect failure secrecy", .timeLimit(.minutes(1))) +@Suite("Connect failure secrecy", .timeLimit(.minutes(5))) struct ConnectFailureSecrecyTests { /// Values that must never appear in a failure text. Each one is handed /// to the code that throws, in the field that really carries it. diff --git a/Tests/macSCPCoreTests/ConnectionDiagnosticsTests.swift b/Tests/macSCPCoreTests/ConnectionDiagnosticsTests.swift index 9d68778f..46d36ea8 100644 --- a/Tests/macSCPCoreTests/ConnectionDiagnosticsTests.swift +++ b/Tests/macSCPCoreTests/ConnectionDiagnosticsTests.swift @@ -176,7 +176,7 @@ struct ConnectionDiagnosticsTests { /// runner"). @Test( .enabled(if: ProcessInfo.processInfo.environment["MACSCP_NETSPIKE"] == "1"), - .timeLimit(.minutes(1))) + .timeLimit(.minutes(5))) func tcpPingToAnUnroutableAddressTimesOutInsideTheStepTimeout() async throws { let outcome = await HostResolver.resolve(host: "192.0.2.1", port: 22, timeout: .seconds(2)) guard case .resolved(let addresses) = outcome, let address = addresses.first else { @@ -1509,7 +1509,7 @@ struct ConnectionDiagnosticsTests { /// into a red rather than a hang: a step that waited for its probe /// would never return, because the gate it waits on is opened only /// after the step has returned. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func aProbeThatIgnoresCancellationDoesNotHoldTheStepPastItsDeadline() async throws { let port = try #require(LoopbackSocket.closedPort()) let release = Gate() diff --git a/Tests/macSCPCoreTests/ConnectionViewModelFailureAcknowledgementTests.swift b/Tests/macSCPCoreTests/ConnectionViewModelFailureAcknowledgementTests.swift index e178c518..3bf8d382 100644 --- a/Tests/macSCPCoreTests/ConnectionViewModelFailureAcknowledgementTests.swift +++ b/Tests/macSCPCoreTests/ConnectionViewModelFailureAcknowledgementTests.swift @@ -18,7 +18,7 @@ import Testing /// Driven through the real `connect()` and `showFailure`, as the /// `lastFailureKind` tests in `ConnectionViewModelTests` are: what matters /// is that the marker reaches the property on the path the App reads. -@Suite("Connection failure acknowledgement", .timeLimit(.minutes(1))) +@Suite("Connection failure acknowledgement", .timeLimit(.minutes(5))) @MainActor struct ConnectionViewModelFailureAcknowledgementTests { private func makeVM( diff --git a/Tests/macSCPCoreTests/ConnectionViewModelManagedKeyStoreTests.swift b/Tests/macSCPCoreTests/ConnectionViewModelManagedKeyStoreTests.swift index c46f16b3..492a434d 100644 --- a/Tests/macSCPCoreTests/ConnectionViewModelManagedKeyStoreTests.swift +++ b/Tests/macSCPCoreTests/ConnectionViewModelManagedKeyStoreTests.swift @@ -21,7 +21,7 @@ import Testing /// "the key loaded with the passphrase the form held". The key is generated /// at run time by `ssh-keygen`; the store is a temporary directory /// (`CorruptManagedKeyStoreRig`). -@Suite("ConnectionViewModel over an unreadable managed key store", .timeLimit(.minutes(1))) +@Suite("ConnectionViewModel over an unreadable managed key store", .timeLimit(.minutes(5))) @MainActor struct ConnectionViewModelManagedKeyStoreTests { private static let loadingConnector: ConnectionViewModel.Connector = { config, _ in diff --git a/Tests/macSCPCoreTests/ConnectionViewModelTests.swift b/Tests/macSCPCoreTests/ConnectionViewModelTests.swift index 31fbc2e8..59fa44f1 100644 --- a/Tests/macSCPCoreTests/ConnectionViewModelTests.swift +++ b/Tests/macSCPCoreTests/ConnectionViewModelTests.swift @@ -4,7 +4,7 @@ import MacSCPTestSupport import Testing @testable import macSCPCore -@Suite("ConnectionViewModel", .timeLimit(.minutes(1))) +@Suite("ConnectionViewModel", .timeLimit(.minutes(5))) @MainActor struct ConnectionViewModelTests { private func makeVM( diff --git a/Tests/macSCPCoreTests/DiagnosticLogSharedSinkTests.swift b/Tests/macSCPCoreTests/DiagnosticLogSharedSinkTests.swift index 207c5d11..d3576fc5 100644 --- a/Tests/macSCPCoreTests/DiagnosticLogSharedSinkTests.swift +++ b/Tests/macSCPCoreTests/DiagnosticLogSharedSinkTests.swift @@ -57,7 +57,7 @@ import Testing /// `DiagnosticLog`'s own private `yyyy-MM-dd` formatting, documented on /// its class-level doc comment as the file-naming contract) — never by /// reading the live singleton's state back. -@Suite("DiagnosticLog shared sink", .serialized, .timeLimit(.minutes(1))) +@Suite("DiagnosticLog shared sink", .serialized, .timeLimit(.minutes(5))) struct DiagnosticLogSharedSinkTests { private func makeTempDirectory() -> URL { let url = FileManager.default.temporaryDirectory diff --git a/Tests/macSCPCoreTests/DiagnosticLogTests.swift b/Tests/macSCPCoreTests/DiagnosticLogTests.swift index 8b19187d..930f72a1 100644 --- a/Tests/macSCPCoreTests/DiagnosticLogTests.swift +++ b/Tests/macSCPCoreTests/DiagnosticLogTests.swift @@ -32,12 +32,17 @@ import Testing /// file `DiagnosticLogSharedSinkIsolationGuardTests` lets mention /// `DiagnosticLog.shared`. /// -/// `.timeLimit(.minutes(1))`: nothing here waits on a wall clock — every +/// `.timeLimit(.minutes(5))`: nothing here waits on a wall clock — every /// wait is either an `await flush()` (which the writer resolves through a /// sequence number, never a timer) or a synchronous read — so the limit is -/// a backstop against a genuine hang, not a duration this suite is expected -/// to approach. -@Suite("DiagnosticLog", .timeLimit(.minutes(1))) +/// a backstop against a genuine hang. The sentence ended "not a duration +/// this suite is expected to approach", which was true of the suite's work +/// and false of what the harness reports: an awaiting case on a three-core +/// runner reports the whole run's wall clock, so at one minute this suite +/// would have approached its bound without anything here being slow +/// (CLAUDE.md, "A wall-clock ceiling in a test measures the runner", +/// 2026-10-30). +@Suite("DiagnosticLog", .timeLimit(.minutes(5))) struct DiagnosticLogTests { /// A fresh, empty directory under the system temp directory, removed by /// the caller in a `defer` — never `~/Library/Logs/macSCP` itself, so a diff --git a/Tests/macSCPCoreTests/DiagnosticsNoDescribingGuardTests.swift b/Tests/macSCPCoreTests/DiagnosticsNoDescribingGuardTests.swift index 00a57609..7f110ad0 100644 --- a/Tests/macSCPCoreTests/DiagnosticsNoDescribingGuardTests.swift +++ b/Tests/macSCPCoreTests/DiagnosticsNoDescribingGuardTests.swift @@ -84,7 +84,7 @@ import Testing /// (a path separator, a query marker, a fragment marker, and whitespace as /// the fourth test). It is not an opening raw-string delimiter, so both /// stripping modes read every file here without throwing. -@Suite("Diagnostics no String(describing:) guard", .timeLimit(.minutes(1))) +@Suite("Diagnostics no String(describing:) guard", .timeLimit(.minutes(5))) struct DiagnosticsNoDescribingGuardTests { /// `#filePath` here is /// `/Tests/macSCPCoreTests/DiagnosticsNoDescribingGuardTests.swift`; diff --git a/Tests/macSCPCoreTests/DialFailureGroupReleaseTests.swift b/Tests/macSCPCoreTests/DialFailureGroupReleaseTests.swift index 03f0fdd1..38ccb778 100644 --- a/Tests/macSCPCoreTests/DialFailureGroupReleaseTests.swift +++ b/Tests/macSCPCoreTests/DialFailureGroupReleaseTests.swift @@ -36,7 +36,7 @@ import Testing /// is not a call of the delayed release, so the spy sees nothing for it; the /// source guard `DialGroupReleaseGuardTests` pins that no such shutdown is /// written in the dial's files at all. -@Suite("Dial failure group release", .timeLimit(.minutes(1))) +@Suite("Dial failure group release", .timeLimit(.minutes(5))) struct DialFailureGroupReleaseTests { enum Path: String, CaseIterable, Sendable, CustomTestStringConvertible { case forwarding diff --git a/Tests/macSCPCoreTests/EmbeddedKeyPorterTests.swift b/Tests/macSCPCoreTests/EmbeddedKeyPorterTests.swift index bbaf432e..1904a4dd 100644 --- a/Tests/macSCPCoreTests/EmbeddedKeyPorterTests.swift +++ b/Tests/macSCPCoreTests/EmbeddedKeyPorterTests.swift @@ -3,7 +3,7 @@ import MacSCPTestSupport import Testing @testable import macSCPCore -@Suite("EmbeddedKeyPorter", .timeLimit(.minutes(1))) +@Suite("EmbeddedKeyPorter", .timeLimit(.minutes(5))) struct EmbeddedKeyPorterTests { private func tempDir() -> URL { let dir = URL(fileURLWithPath: NSTemporaryDirectory()) diff --git a/Tests/macSCPCoreTests/ExtractPreviewTests.swift b/Tests/macSCPCoreTests/ExtractPreviewTests.swift index a3db49b6..72fee89e 100644 --- a/Tests/macSCPCoreTests/ExtractPreviewTests.swift +++ b/Tests/macSCPCoreTests/ExtractPreviewTests.swift @@ -2,7 +2,7 @@ import Testing import Foundation @testable import macSCPCore -@Suite(.timeLimit(.minutes(1))) +@Suite(.timeLimit(.minutes(5))) struct ExtractPreviewTests { @Test func theListingCommandForAZipAsksUnzipForNamesOnly() throws { let plan = try ArchivePlan.listing( diff --git a/Tests/macSCPCoreTests/FileActionActivityTests.swift b/Tests/macSCPCoreTests/FileActionActivityTests.swift index 2d3396db..fa9768c9 100644 --- a/Tests/macSCPCoreTests/FileActionActivityTests.swift +++ b/Tests/macSCPCoreTests/FileActionActivityTests.swift @@ -10,7 +10,7 @@ import Testing /// injected, and the one real runner's pure half is tested through its /// `resolve` seam. @MainActor -@Suite(.timeLimit(.minutes(1))) +@Suite(.timeLimit(.minutes(5))) struct FileActionActivityTests { private static let first = FileActionInvocation(executable: "wc", arguments: ["-l", "/d/a"]) private static let second = FileActionInvocation(executable: "wc", arguments: ["-l", "/d/b"]) @@ -110,7 +110,7 @@ struct FileActionActivityTests { /// The runner here ignores the cancel, so what stops the second /// invocation is the activity's own check between the two. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func cancellingDuringARunStartsNoFurtherInvocation() async { let activity = FileActionActivity() let runner = RecordingRunner(parksFirst: true) diff --git a/Tests/macSCPCoreTests/FileDiffTests.swift b/Tests/macSCPCoreTests/FileDiffTests.swift index 5727fba5..e4e82b93 100644 --- a/Tests/macSCPCoreTests/FileDiffTests.swift +++ b/Tests/macSCPCoreTests/FileDiffTests.swift @@ -357,7 +357,7 @@ import Testing /// context and additions, and it spends no more edits than the /// shortest edit script does. Run over a two-letter alphabet, which is /// where the ties live. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func aWholeFileHunkReconstructsBothSidesWithAMinimalScript() { var rng = Lcg(state: 20_261_013) var differing = 0 @@ -393,7 +393,7 @@ import Testing /// it was, and the hunks are in order and do not overlap. And nothing is /// lost between them: `expectEveryChangeIsShownOnce` checks, against the /// whole-file script, that no change falls outside the hunks. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func everyHunkPointsAtItsOwnLinesOnBothSides() { var rng = Lcg(state: 20_261_014) var multiHunk = 0 @@ -571,7 +571,7 @@ import Testing /// Swapping the sides swaps the kinds: what one direction removes the /// other adds, and the same number of lines is touched. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func swappingTheSidesSwapsAddedAndRemoved() { var rng = Lcg(state: 20_261_015) var touched = 0 diff --git a/Tests/macSCPCoreTests/GenerateKeyFormTests.swift b/Tests/macSCPCoreTests/GenerateKeyFormTests.swift index d0300ab9..6e7f7e01 100644 --- a/Tests/macSCPCoreTests/GenerateKeyFormTests.swift +++ b/Tests/macSCPCoreTests/GenerateKeyFormTests.swift @@ -11,7 +11,7 @@ import Testing /// fields changed during the run" is the test's own sequence, not a race. /// Nothing here runs `ssh-keygen`: the generator writes two placeholder files /// where the real one would, so the tests can see whether they are removed. -@Suite("GenerateKeyForm", .timeLimit(.minutes(1))) +@Suite("GenerateKeyForm", .timeLimit(.minutes(5))) @MainActor struct GenerateKeyFormTests { /// Never written into an `#expect` expression's source text (CLAUDE.md, diff --git a/Tests/macSCPCoreTests/HostAddressLookupTests.swift b/Tests/macSCPCoreTests/HostAddressLookupTests.swift index 66dd77ee..a2044d0d 100644 --- a/Tests/macSCPCoreTests/HostAddressLookupTests.swift +++ b/Tests/macSCPCoreTests/HostAddressLookupTests.swift @@ -23,7 +23,7 @@ import Testing /// depend on how fast the runner is. The suite's time limit is the /// harness's, for a regression that stops cutting it: it turns a hang into /// a red, and asserts nothing about how long the cut took. -@Suite("Host address lookup for the connection form", .timeLimit(.minutes(1))) +@Suite("Host address lookup for the connection form", .timeLimit(.minutes(5))) struct HostAddressLookupTests { private static let roomy: Duration = .seconds(600) private static let tight: Duration = .milliseconds(200) diff --git a/Tests/macSCPCoreTests/ITermColorsImportTests.swift b/Tests/macSCPCoreTests/ITermColorsImportTests.swift index 4404253f..dd678fe4 100644 --- a/Tests/macSCPCoreTests/ITermColorsImportTests.swift +++ b/Tests/macSCPCoreTests/ITermColorsImportTests.swift @@ -21,7 +21,7 @@ import Testing /// read here. What is verified is the SHAPE described above, against files /// this suite authored and `plutil -lint` accepted — not that iTerm2's own /// export is byte-identical to them. -@Suite("iTerm2 colour import", .timeLimit(.minutes(1))) +@Suite("iTerm2 colour import", .timeLimit(.minutes(5))) struct ITermColorsImportTests { private static func fixture(_ name: String) throws -> Data { let url = URL(fileURLWithPath: #filePath) diff --git a/Tests/macSCPCoreTests/InternetSpeedLiveTransportTests.swift b/Tests/macSCPCoreTests/InternetSpeedLiveTransportTests.swift index b17a7f00..f3b2d400 100644 --- a/Tests/macSCPCoreTests/InternetSpeedLiveTransportTests.swift +++ b/Tests/macSCPCoreTests/InternetSpeedLiveTransportTests.swift @@ -24,7 +24,7 @@ import Testing /// Caching cannot cross a run here the way it can in the S3 suite: the live /// transport's session is ephemeral with `urlCache = nil`, and every /// request carries `.reloadIgnoringLocalAndRemoteCacheData`. -@Suite("The internet speed test's live transport", .timeLimit(.minutes(1))) +@Suite("The internet speed test's live transport", .timeLimit(.minutes(5))) struct InternetSpeedLiveTransportTests { /// A 307, which is the dangerous one: 301/302/303 rewrite the request /// to a GET and drop the body, while 307 and 308 preserve the method diff --git a/Tests/macSCPCoreTests/KeyPassphraseFormsTests.swift b/Tests/macSCPCoreTests/KeyPassphraseFormsTests.swift index abf62404..17949fa9 100644 --- a/Tests/macSCPCoreTests/KeyPassphraseFormsTests.swift +++ b/Tests/macSCPCoreTests/KeyPassphraseFormsTests.swift @@ -16,7 +16,7 @@ import Testing /// `Bool` computed first, so neither a value nor its spelling can reach a /// failure message (CLAUDE.md, "A value a test must not leak has two exits, /// not one"). That rule is the whole subject matter here. -@Suite("Managed key passphrase forms", .timeLimit(.minutes(1))) +@Suite("Managed key passphrase forms", .timeLimit(.minutes(5))) @MainActor struct KeyPassphraseFormsTests { /// What the key file actually opens with, before anything in a test runs. diff --git a/Tests/macSCPCoreTests/LocalFileSystemTests.swift b/Tests/macSCPCoreTests/LocalFileSystemTests.swift index e511a704..5db3e0b3 100644 --- a/Tests/macSCPCoreTests/LocalFileSystemTests.swift +++ b/Tests/macSCPCoreTests/LocalFileSystemTests.swift @@ -4,12 +4,12 @@ import MacSCPTestSupport import Testing @testable import macSCPCore -/// `.timeLimit(.minutes(1))`: the parked-entry tests below abandon a child +/// `.timeLimit(.minutes(5))`: the parked-entry tests below abandon a child /// `Task` on a `Gate` the test never opens — the ONLY clock that ends such a /// test is cancellation from this trait, never a wall-clock bound of the /// test's own (CLAUDE.md, "A wall-clock ceiling in a test measures the /// runner"). -@Suite("LocalFileSystem", .timeLimit(.minutes(1))) +@Suite("LocalFileSystem", .timeLimit(.minutes(5))) struct LocalFileSystemTests { /// Creates a throwaway tree: /unterordner/ and /datei.txt (5 bytes). private func makeTempTree() throws -> URL { diff --git a/Tests/macSCPCoreTests/LocalForwardListenerTests.swift b/Tests/macSCPCoreTests/LocalForwardListenerTests.swift index eda2578d..f460cf11 100644 --- a/Tests/macSCPCoreTests/LocalForwardListenerTests.swift +++ b/Tests/macSCPCoreTests/LocalForwardListenerTests.swift @@ -17,7 +17,7 @@ import Testing /// Every listener binds port 0 and every wait is an `await`; nothing here /// holds a fixed port or a wall-clock bound of its own (the suite's /// `.timeLimit` is the only deadline). -@Suite("LocalForwardListener", .timeLimit(.minutes(1))) +@Suite("LocalForwardListener", .timeLimit(.minutes(5))) struct LocalForwardListenerTests { @Test func bytesTravelToTheFactorysChannelAndBack() async throws { diff --git a/Tests/macSCPCoreTests/ManagedKeyStoreUnreadableTests.swift b/Tests/macSCPCoreTests/ManagedKeyStoreUnreadableTests.swift index 5059c497..87052f7c 100644 --- a/Tests/macSCPCoreTests/ManagedKeyStoreUnreadableTests.swift +++ b/Tests/macSCPCoreTests/ManagedKeyStoreUnreadableTests.swift @@ -21,7 +21,7 @@ import Testing /// named constant that no expectation spells (CLAUDE.md, "A value a test /// must not leak has two exits"): every check that the content does not /// leak computes its `Bool` first. -@Suite("An unreadable managed key store", .timeLimit(.minutes(1))) +@Suite("An unreadable managed key store", .timeLimit(.minutes(5))) struct ManagedKeyStoreUnreadableTests { // MARK: - The link remembers what it saw diff --git a/Tests/macSCPCoreTests/NetworkTraceTests.swift b/Tests/macSCPCoreTests/NetworkTraceTests.swift index 9fc10142..25a2d6ee 100644 --- a/Tests/macSCPCoreTests/NetworkTraceTests.swift +++ b/Tests/macSCPCoreTests/NetworkTraceTests.swift @@ -530,7 +530,7 @@ struct NetworkTraceTests { /// asserted before — the hops survive, and the ending names the budget — /// with no duration compared against any other duration anywhere in the /// case. - @Test(.timeLimit(.minutes(1))) + @Test(.timeLimit(.minutes(5))) func anOuterMarginOverrunKeepsTheHopsTheWalkHadMeasured() async { let measured = NetworkTraceHop( ttl: 1, outcome: .forwarded(address: "10.0.0.1", rtt: .milliseconds(2))) diff --git a/Tests/macSCPCoreTests/OneAtATimeTests.swift b/Tests/macSCPCoreTests/OneAtATimeTests.swift index 1d36990e..6ca9f8c6 100644 --- a/Tests/macSCPCoreTests/OneAtATimeTests.swift +++ b/Tests/macSCPCoreTests/OneAtATimeTests.swift @@ -5,7 +5,7 @@ import Testing /// `OneAtATime`, the latch login-set import runs through. The first run is /// held open on a signal the test raises, so "the second call arrived while /// the first was in flight" is the test's own sequence, not a race. -@Suite("OneAtATime", .timeLimit(.minutes(1))) +@Suite("OneAtATime", .timeLimit(.minutes(5))) @MainActor struct OneAtATimeTests { @Test func anOverlappingRunIsRefusedWithoutRunning() async { diff --git a/Tests/macSCPCoreTests/PKCS12ImportIsolationGuardTests.swift b/Tests/macSCPCoreTests/PKCS12ImportIsolationGuardTests.swift index 77d29a47..9465e9f6 100644 --- a/Tests/macSCPCoreTests/PKCS12ImportIsolationGuardTests.swift +++ b/Tests/macSCPCoreTests/PKCS12ImportIsolationGuardTests.swift @@ -112,7 +112,7 @@ import Testing /// stops the scan. Both cost a false alarm, neither a silent pass — and /// this paragraph is the one to distrust first, since it is the claim that /// was wrong the last time it was written. -@Suite("PKCS#12 import isolation", .timeLimit(.minutes(1))) +@Suite("PKCS#12 import isolation", .timeLimit(.minutes(5))) struct PKCS12ImportIsolationGuardTests { /// `#filePath` is /// `/Tests/macSCPCoreTests/PKCS12ImportIsolationGuardTests.swift`. diff --git a/Tests/macSCPCoreTests/PTYSubprocessTests.swift b/Tests/macSCPCoreTests/PTYSubprocessTests.swift index 49fe07d1..65e69fdb 100644 --- a/Tests/macSCPCoreTests/PTYSubprocessTests.swift +++ b/Tests/macSCPCoreTests/PTYSubprocessTests.swift @@ -9,7 +9,7 @@ import Testing /// long a case takes (CLAUDE.md, "A wall-clock ceiling in a test measures /// the runner") — every wait in here is an `await` on `PTYSubprocess.run` /// or on its `Handle.output`, never a sleep guessed to be "long enough". -@Suite("PTYSubprocess", .timeLimit(.minutes(1))) +@Suite("PTYSubprocess", .timeLimit(.minutes(5))) struct PTYSubprocessTests { /// The positive this whole file exists to establish: `/usr/bin/tty` /// prints a real `/dev/ttysNNN` path, which it does if and only if its diff --git a/Tests/macSCPCoreTests/PollUntilTests.swift b/Tests/macSCPCoreTests/PollUntilTests.swift index a5854a9a..d4b3774f 100644 --- a/Tests/macSCPCoreTests/PollUntilTests.swift +++ b/Tests/macSCPCoreTests/PollUntilTests.swift @@ -1,7 +1,7 @@ import Testing import MacSCPTestSupport -@Suite("pollUntil", .timeLimit(.minutes(1))) +@Suite("pollUntil", .timeLimit(.minutes(5))) struct PollUntilTests { /// A condition that holds on the third evaluation returns after /// exactly three evaluations — the helper polls, it does not race. diff --git a/Tests/macSCPCoreTests/PollingGuardTests.swift b/Tests/macSCPCoreTests/PollingGuardTests.swift index a2e59436..f93218fb 100644 --- a/Tests/macSCPCoreTests/PollingGuardTests.swift +++ b/Tests/macSCPCoreTests/PollingGuardTests.swift @@ -16,13 +16,13 @@ import Testing /// `.wait(until: Date(...)` (CLAUDE.md, "A wall-clock ceiling in a test /// measures the runner"). /// -/// `.timeLimit(.minutes(1))` because every check here scans every Swift +/// `.timeLimit(.minutes(5))` because every check here scans every Swift /// file under `Tests/`. The files, their text and their blanked view come /// from `SourceCorpus`, each file read and blanked at most once per test /// process; the patterns are compiled once per process (`CompiledPattern`) /// rather than on every call; and the while-block scan two checks share is /// remembered per file (`sleepingWhileBlocksByFile`). -@Suite("Polling guard", .timeLimit(.minutes(1))) +@Suite("Polling guard", .timeLimit(.minutes(5))) struct PollingGuardTests { private static var testsRoot: URL { URL(fileURLWithPath: #filePath) diff --git a/Tests/macSCPCoreTests/ProbeDeadlineTests.swift b/Tests/macSCPCoreTests/ProbeDeadlineTests.swift index 72f5ea65..ecf328f6 100644 --- a/Tests/macSCPCoreTests/ProbeDeadlineTests.swift +++ b/Tests/macSCPCoreTests/ProbeDeadlineTests.swift @@ -30,7 +30,7 @@ import Testing /// `ProbeDeadlineSaturationTests` holds it to the same contract with the /// pool full, and is gated, because a test that fills the pool ends every /// other test sharing the process. -@Suite("Probe deadlines", .timeLimit(.minutes(1))) +@Suite("Probe deadlines", .timeLimit(.minutes(5))) struct ProbeDeadlineTests { /// A scheduled deadline fires; a cancelled one does not, even when its /// own moment has passed. @@ -210,7 +210,7 @@ struct ProbeDeadlineTests { /// at all — the probe never returns while the pool is parked, and the suite's /// time limit is what ends the test. Measured 2026-09-25, both cases red /// that way before the timer was written. -@Suite("Probe deadlines under a saturated Dispatch pool", .serialized, .timeLimit(.minutes(1))) +@Suite("Probe deadlines under a saturated Dispatch pool", .serialized, .timeLimit(.minutes(5))) struct ProbeDeadlineSaturationTests { private static let gate = ProcessInfo.processInfo.environment["MACSCP_SATURATION"] == "1" private static let gateReason: Comment = diff --git a/Tests/macSCPCoreTests/ProbeStartTests.swift b/Tests/macSCPCoreTests/ProbeStartTests.swift index 9f924a90..a085d919 100644 --- a/Tests/macSCPCoreTests/ProbeStartTests.swift +++ b/Tests/macSCPCoreTests/ProbeStartTests.swift @@ -21,7 +21,7 @@ import Testing /// has not begun, whatever the machine is doing. What the cases assert is an /// outcome and an ordering; the one duration any of them reads is a FLOOR, /// which a slow machine cannot defeat. -@Suite("A probe that never began", .timeLimit(.minutes(1))) +@Suite("A probe that never began", .timeLimit(.minutes(5))) struct ProbeStartTests { /// Short, because nothing here waits for it on a clock: each case waits /// for the probe to return, however late that is. diff --git a/Tests/macSCPCoreTests/RemoteBrowserViewModelMetadataMergeTests.swift b/Tests/macSCPCoreTests/RemoteBrowserViewModelMetadataMergeTests.swift index 91bfd47e..6e483bcd 100644 --- a/Tests/macSCPCoreTests/RemoteBrowserViewModelMetadataMergeTests.swift +++ b/Tests/macSCPCoreTests/RemoteBrowserViewModelMetadataMergeTests.swift @@ -13,29 +13,64 @@ import Testing /// tests, but one layer up, at the view model. /// /// `.timeLimit(.minutes(5))` in its own suite rather than added to the much -/// larger `RemoteBrowserViewModelTests` suite: only these tests poll via -/// `pollUntil` for a background merge Task to catch up, so only they need a -/// stated ceiling on how long that poll may run before the harness itself +/// larger `RemoteBrowserViewModelTests` suite: these tests poll via +/// `pollUntil` for a background merge Task to catch up, so they need a +/// stated bound on how long that poll may run before the harness itself /// calls it a hang (CLAUDE.md: "Tests never block the cooperative pool" — /// every wait here is `pollUntil`'s `await Task.sleep`, never a blocking /// wait, and the `.timeLimit` is the only clock any assertion reads). /// +/// That sentence read "only these tests poll via `pollUntil` for a +/// background merge Task to catch up, so only they need a stated ceiling" +/// until 2026-10-30 — the withdrawn wording, and a claim about the rest of +/// the tree that nobody counted while writing it. Counted on 2026-10-30: +/// 127 bounds of this kind stand in `Tests/`, 110 of them raised from one +/// minute in the commit that corrects this paragraph. +/// +/// grep -rnE '\.timeLimit\(\.minutes\(5\)\)' Tests/ | grep -vc '///' +/// +/// A line count, not an occurrence count, and that is deliberate: a bound +/// is one trait on one line, while the same spelling also appears in nine +/// doc comments that describe their own suite's bound, and those must stay +/// out of the figure. Writing this paragraph changed its own answer twice +/// before the count stopped moving — 128 matches, then 136 — which is the +/// `grep -c` distinction in CLAUDE.md arriving from the other side: here +/// lines ARE the unit, so `-c` is the right tool and `-o | wc -l` is the +/// wrong one. The pattern is written escaped so the command does not match +/// its own text, and `grep -vc '///'` would drop that line regardless. +/// /// The bound read `.minutes(1)` until 2026-10-29 — the paragraph above /// opened "`.timeLimit(.minutes(1))` in its own suite" — when all seven /// polling cases here came back `Time limit was exceeded: 60.000 seconds` -/// on CI run 40933631584, the first run of the gate on `macos-latest`. One -/// minute was not a hang bound for them; it was a wall-clock ceiling, which -/// is the thing CLAUDE.md has a rule against. On the development machine +/// on CI run 40933631584, the first run of the gate on `macos-latest`. On +/// the development machine /// /// swift test --filter RemoteBrowserViewModelMetadataMergeTests /// /// reports the whole suite in 0.087 s, the slowest of the seven /// (`twoHundredEntriesArrivingInReverseOrderMergeUnderSizeSort`) in that -/// same 0.087 s, and the other six in 0.011 s to 0.013 s. Sixty seconds is -/// 690x the slowest case measured here and still lost to a loaded -/// three-core runner. Five minutes is 3448x it, and turns a poll that -/// never completes into a red just as well, which is all this bound is -/// for. `ConnectionDiagnostics` carries `.minutes(5)` for the same reason. +/// same 0.087 s, and the other six in 0.011 s to 0.013 s. +/// +/// Two of the conclusions drawn from those figures are withdrawn as of +/// 2026-10-30. The first attributed the red to this suite: "One minute was +/// not a hang bound for them; it was a wall-clock ceiling, which is the +/// thing CLAUDE.md has a rule against". The second derived the new value +/// by ratio: "Sixty seconds is 690x the slowest case measured here and +/// still lost to a loaded three-core runner. Five minutes is 3448x it". +/// The direction was right; the attribution and the arithmetic were not. +/// One minute was not too tight for these seven cases, it was too tight +/// for any awaiting test on that runner — and a ratio against the local +/// figure measures nothing, because the runner figure it divides into is +/// the whole run's duration rather than the case's own work. +/// +/// What survives from that sentence is the half that never rested on a +/// ratio: five minutes "turns a poll that never completes into a red just +/// as well, which is all this bound is for", and `ConnectionDiagnostics` +/// carried `.minutes(5)` before any of this. The runs that show the rest, +/// and the mechanism behind them, are in CLAUDE.md, "A wall-clock ceiling +/// in a test measures the runner", 2026-10-30 update; what belongs here is +/// only that this suite needs a bound at all, which the paragraph above +/// says. /// /// One cross-reference checked while changing this, since it is a claim /// about other code: `twoHundredEntries…`'s own comment below cites diff --git a/Tests/macSCPCoreTests/RemoteForwardTests.swift b/Tests/macSCPCoreTests/RemoteForwardTests.swift index 5f1bc242..436d0c87 100644 --- a/Tests/macSCPCoreTests/RemoteForwardTests.swift +++ b/Tests/macSCPCoreTests/RemoteForwardTests.swift @@ -29,7 +29,7 @@ import Testing /// its own. The remote port is named, and the fake answers with a DIFFERENT /// number, so `start` is measured to return what `onOpen` said rather than /// what it was asked for. -@Suite("RemoteForward", .timeLimit(.minutes(1))) +@Suite("RemoteForward", .timeLimit(.minutes(5))) struct RemoteForwardTests { /// The whole path: the server names a port, an inbound connection is diff --git a/Tests/macSCPCoreTests/S3RedirectAuthorizationMeasurementTests.swift b/Tests/macSCPCoreTests/S3RedirectAuthorizationMeasurementTests.swift index 18471c52..2988e085 100644 --- a/Tests/macSCPCoreTests/S3RedirectAuthorizationMeasurementTests.swift +++ b/Tests/macSCPCoreTests/S3RedirectAuthorizationMeasurementTests.swift @@ -50,7 +50,7 @@ import Testing /// What this cannot see: what a REAL S3 provider does on a redirect. This /// measures Foundation against a controlled loopback stub, which is what the /// question is about, and nothing beyond it. -@Suite("S3 redirect Authorization measurement", .timeLimit(.minutes(1))) +@Suite("S3 redirect Authorization measurement", .timeLimit(.minutes(5))) struct S3RedirectAuthorizationMeasurementTests { // MARK: - Canned responses diff --git a/Tests/macSCPCoreTests/S3RedirectControlTests.swift b/Tests/macSCPCoreTests/S3RedirectControlTests.swift index ecc2672a..1b42cd26 100644 --- a/Tests/macSCPCoreTests/S3RedirectControlTests.swift +++ b/Tests/macSCPCoreTests/S3RedirectControlTests.swift @@ -22,7 +22,7 @@ import Testing /// /// What this cannot see: what a REAL S3 provider does on a redirect. It /// measures this project's policy against a controlled stub. -@Suite("S3 redirect control", .timeLimit(.minutes(1))) +@Suite("S3 redirect control", .timeLimit(.minutes(5))) struct S3RedirectControlTests { private static func freshBucket() -> String { "bucket-\(UUID().uuidString)" } diff --git a/Tests/macSCPCoreTests/S3SessionIsolationTests.swift b/Tests/macSCPCoreTests/S3SessionIsolationTests.swift index 16446027..ac4122f8 100644 --- a/Tests/macSCPCoreTests/S3SessionIsolationTests.swift +++ b/Tests/macSCPCoreTests/S3SessionIsolationTests.swift @@ -24,7 +24,7 @@ import Testing /// Neither test clears `URLCache.shared`: it belongs to whoever is running /// the suite. They only read it, and each dial uses a bucket name nothing /// has used before so the key they look up is unrepeatable. -@Suite("S3 session isolation", .timeLimit(.minutes(1))) +@Suite("S3 session isolation", .timeLimit(.minutes(5))) struct S3SessionIsolationTests { /// A well-formed, EXPLICITLY cacheable empty `ListObjectsV2` answer. diff --git a/Tests/macSCPCoreTests/SFTPStartBoundTests.swift b/Tests/macSCPCoreTests/SFTPStartBoundTests.swift index d327b986..30597b11 100644 --- a/Tests/macSCPCoreTests/SFTPStartBoundTests.swift +++ b/Tests/macSCPCoreTests/SFTPStartBoundTests.swift @@ -20,7 +20,7 @@ import Testing /// Every deadline here is fired BY HAND through an injected sleeper: nothing /// in this suite waits for the connect timeout, or for any wall-clock time. /// `.timeLimit` is a hang bound only. -@Suite("SFTP start bound", .timeLimit(.minutes(1))) +@Suite("SFTP start bound", .timeLimit(.minutes(5))) struct SFTPStartBoundTests { // MARK: - The seam diff --git a/Tests/macSCPCoreTests/SOCKS5HandshakeTests.swift b/Tests/macSCPCoreTests/SOCKS5HandshakeTests.swift index a1ecbb0a..6ab8b113 100644 --- a/Tests/macSCPCoreTests/SOCKS5HandshakeTests.swift +++ b/Tests/macSCPCoreTests/SOCKS5HandshakeTests.swift @@ -22,7 +22,7 @@ import Testing /// `BytePumpTests` gives: `EmbeddedEventLoop.checkCorrectThread` only /// tolerates the thread it was created on, and an `await` in a Swift /// Testing body can resume elsewhere. -@Suite("SOCKS5 handshake", .timeLimit(.minutes(1))) +@Suite("SOCKS5 handshake", .timeLimit(.minutes(5))) struct SOCKS5HandshakeTests { // MARK: - Turning reading on diff --git a/Tests/macSCPCoreTests/SOCKS5ListenerTests.swift b/Tests/macSCPCoreTests/SOCKS5ListenerTests.swift index e9883189..0cc06933 100644 --- a/Tests/macSCPCoreTests/SOCKS5ListenerTests.swift +++ b/Tests/macSCPCoreTests/SOCKS5ListenerTests.swift @@ -16,7 +16,7 @@ import Testing /// /// Every listener binds port 0 and every wait is an `await`; nothing here /// holds a fixed port or a wall-clock bound of its own. -@Suite("SOCKS5Listener", .timeLimit(.minutes(1))) +@Suite("SOCKS5Listener", .timeLimit(.minutes(5))) struct SOCKS5ListenerTests { /// Greeting, CONNECT, the two replies, then ordinary bytes through the @@ -399,7 +399,7 @@ private struct FactoryRefusedTheChannel: Error {} /// The handover box on its own: a waiter whose task is cancelled is released /// with the cancellation, and the box stays resolved exactly once — a later /// resolve is dropped. -@Suite("SOCKS5RequestBox", .timeLimit(.minutes(1))) +@Suite("SOCKS5RequestBox", .timeLimit(.minutes(5))) struct SOCKS5RequestBoxTests { @Test func cancellingTheWaitingTaskResolvesTheBoxOnce() async throws { let box = SOCKS5RequestBox() diff --git a/Tests/macSCPCoreTests/SSHForwardingConnectionTests.swift b/Tests/macSCPCoreTests/SSHForwardingConnectionTests.swift index b2a7f438..8200d765 100644 --- a/Tests/macSCPCoreTests/SSHForwardingConnectionTests.swift +++ b/Tests/macSCPCoreTests/SSHForwardingConnectionTests.swift @@ -32,7 +32,7 @@ import Testing /// /// The gated twin, against a real OpenSSH without the SFTP subsystem, is /// `ForwardingWithoutSFTPITests`. -@Suite("SSHForwardingConnection", .timeLimit(.minutes(1))) +@Suite("SSHForwardingConnection", .timeLimit(.minutes(5))) struct SSHForwardingConnectionTests { enum Path: String, CaseIterable, Sendable, CustomTestStringConvertible { case forwarding diff --git a/Tests/macSCPCoreTests/SSHPrivateKeyLoaderTests.swift b/Tests/macSCPCoreTests/SSHPrivateKeyLoaderTests.swift index 88d20a60..0cbaa63a 100644 --- a/Tests/macSCPCoreTests/SSHPrivateKeyLoaderTests.swift +++ b/Tests/macSCPCoreTests/SSHPrivateKeyLoaderTests.swift @@ -33,11 +33,17 @@ import Testing /// Test keys are generated at RUNTIME (ssh-keygen) — never checked in. /// -/// `.timeLimit(.minutes(1))`: `makeKey` shells out to `ssh-keygen` once per -/// call, well under a second even on a loaded runner — a single invocation -/// taking a full minute would itself be the defect. The project default is -/// enough; nothing here needed the wider limit. -@Suite("SSHPrivateKeyLoader", .timeLimit(.minutes(1))) +/// `.timeLimit(.minutes(5))`: `makeKey` shells out to `ssh-keygen` once per +/// call, well under a second even on a loaded runner. This paragraph went +/// on to read "a single invocation taking a full minute would itself be the +/// defect. The project default is enough; nothing here needed the wider +/// limit" — the withdrawn wording, and its reasoning does not hold: a +/// minute of time reported against a case on a saturated runner is not a +/// minute of `ssh-keygen`, it is the run's own wall clock seen from that +/// case (CLAUDE.md, "A wall-clock ceiling in a test measures the runner", +/// 2026-10-30). There is no wider limit left to decline; five minutes is +/// the figure the tree carries. +@Suite("SSHPrivateKeyLoader", .timeLimit(.minutes(5))) struct SSHPrivateKeyLoaderTests { /// Generates a key of `type` in the temp directory; passphrase "" = unencrypted. /// `extra` is appended to the ssh-keygen argument list (e.g. `["-b", "384"]`, diff --git a/Tests/macSCPCoreTests/TerminalPanelViewModelTests.swift b/Tests/macSCPCoreTests/TerminalPanelViewModelTests.swift index cf4fcd04..4ab7f537 100644 --- a/Tests/macSCPCoreTests/TerminalPanelViewModelTests.swift +++ b/Tests/macSCPCoreTests/TerminalPanelViewModelTests.swift @@ -28,7 +28,7 @@ final class MockShell: RemoteShell, Sendable { func close() async { state.withLock { $0.closed = true }; continuation.finish() } } -@Suite("TerminalPanelViewModel", .timeLimit(.minutes(1))) +@Suite("TerminalPanelViewModel", .timeLimit(.minutes(5))) @MainActor struct TerminalPanelViewModelTests { @Test func toggleOpensShellAndForwardsOutput() async throws { @@ -943,7 +943,7 @@ actor ShellFactory { /// out. The audit log's snippet entry hangs on it: before P5 the entry was /// written right after the send CALL, so a shell that failed to open left an /// entry claiming a snippet ran when its bytes were buffered and discarded. -@Suite("TerminalPanelViewModel delivery callback", .timeLimit(.minutes(1))) +@Suite("TerminalPanelViewModel delivery callback", .timeLimit(.minutes(5))) @MainActor struct TerminalPanelViewModelDeliveryTests { /// Lets the test decide WHEN the shell finishes opening, instead of @@ -1007,7 +1007,7 @@ struct TerminalPanelViewModelDeliveryTests { /// trades a flaky test for a hung suite, in a repository whose backlog /// already carries a suite-hang incident. The ending that measurement /// bought was a private ceiling task of thirty seconds; it is now the - /// suite's `.timeLimit(.minutes(1))`, which cancels the test — and + /// suite's `.timeLimit(.minutes(5))`, which cancels the test — and /// `waitForFirstDelivery` resumes its parked waiter from the /// cancellation handler, so the caller's assertion and its message still /// run. A callback that will NEVER fire is still a red test with a diff --git a/Tests/macSCPCoreTests/TerminalThemeTests.swift b/Tests/macSCPCoreTests/TerminalThemeTests.swift index c6ba9d13..c2f06aab 100644 --- a/Tests/macSCPCoreTests/TerminalThemeTests.swift +++ b/Tests/macSCPCoreTests/TerminalThemeTests.swift @@ -10,7 +10,7 @@ import Testing /// Known blind spot: nothing here renders anything. That a theme reaches /// the emulator is `TerminalThemeWiringGuardTests`' (App, source-scanning); /// that SwiftTerm paints the sixteen colours it is handed is SwiftTerm's. -@Suite("Terminal themes", .timeLimit(.minutes(1))) +@Suite("Terminal themes", .timeLimit(.minutes(5))) struct TerminalThemeTests { // MARK: - Colour diff --git a/Tests/macSCPCoreTests/TerminalTypeTests.swift b/Tests/macSCPCoreTests/TerminalTypeTests.swift index 2b1f0064..1b1ad313 100644 --- a/Tests/macSCPCoreTests/TerminalTypeTests.swift +++ b/Tests/macSCPCoreTests/TerminalTypeTests.swift @@ -14,7 +14,7 @@ import Testing /// terminfo entries (the task report); the gated /// `CitadelShellIntegrationTests.theChosenTerminalTypeReachesTheRemoteShell` /// proves only that the name reaches the server. -@Suite("Terminal type", .timeLimit(.minutes(1))) +@Suite("Terminal type", .timeLimit(.minutes(5))) @MainActor struct TerminalTypeTests { // MARK: - What is offered diff --git a/Tests/macSCPCoreTests/TransferEngineTests.swift b/Tests/macSCPCoreTests/TransferEngineTests.swift index 4ac61b09..ceecd185 100644 --- a/Tests/macSCPCoreTests/TransferEngineTests.swift +++ b/Tests/macSCPCoreTests/TransferEngineTests.swift @@ -3,7 +3,7 @@ import MacSCPTestSupport import Testing @testable import macSCPCore -@Suite("TransferEngine", .timeLimit(.minutes(1))) +@Suite("TransferEngine", .timeLimit(.minutes(5))) struct TransferEngineTests { private func makeSource(content: Data) -> MockRemoteFileSystem { MockRemoteFileSystem( diff --git a/Tests/macSCPCoreTests/TransferQueueViewModelTests.swift b/Tests/macSCPCoreTests/TransferQueueViewModelTests.swift index 7bee4554..da952d4b 100644 --- a/Tests/macSCPCoreTests/TransferQueueViewModelTests.swift +++ b/Tests/macSCPCoreTests/TransferQueueViewModelTests.swift @@ -4,7 +4,7 @@ import Synchronization import Testing @testable import macSCPCore -@Suite("TransferQueueViewModel", .timeLimit(.minutes(1))) +@Suite("TransferQueueViewModel", .timeLimit(.minutes(5))) @MainActor struct TransferQueueViewModelTests { diff --git a/Tests/macSCPCoreTests/TunnelLatchTests.swift b/Tests/macSCPCoreTests/TunnelLatchTests.swift index 56f00c7d..54353bb5 100644 --- a/Tests/macSCPCoreTests/TunnelLatchTests.swift +++ b/Tests/macSCPCoreTests/TunnelLatchTests.swift @@ -16,12 +16,12 @@ import Testing /// is the direct measurement, so a future change to `TunnelLatch` is red /// here rather than red as a puzzling ordering failure two files away. /// -/// `.timeLimit(.minutes(1))` per `PollingGuardTests +/// `.timeLimit(.minutes(5))` per `PollingGuardTests /// .everyCallerOfPollUntilDeclaresATimeLimit` — and, honestly, it would /// not save this suite if the latch regressed: a wait that ignores /// cancellation ignores the time limit's cancellation too. Every case /// releases its latch on the way out. -@Suite("Tunnel latch", .timeLimit(.minutes(1))) +@Suite("Tunnel latch", .timeLimit(.minutes(5))) struct TunnelLatchTests { /// A wait entered from an ALREADY-CANCELLED task stays parked, and the /// release is what ends it — not the cancellation. diff --git a/Tests/macSCPCoreTests/TunnelRunnerTests.swift b/Tests/macSCPCoreTests/TunnelRunnerTests.swift index cd01379d..f0908cc7 100644 --- a/Tests/macSCPCoreTests/TunnelRunnerTests.swift +++ b/Tests/macSCPCoreTests/TunnelRunnerTests.swift @@ -14,7 +14,7 @@ import Testing /// block the cooperative pool"), and no assertion carries a wall-clock /// ceiling: the backoff is measured by what the injected sleeper was ASKED /// for, not by how long anything took. -@Suite("Tunnel runner", .timeLimit(.minutes(1))) +@Suite("Tunnel runner", .timeLimit(.minutes(5))) struct TunnelRunnerTests { // MARK: - The clean sequence diff --git a/Tests/macSCPCoreTests/TunnelRuntimeFactoryWiringTests.swift b/Tests/macSCPCoreTests/TunnelRuntimeFactoryWiringTests.swift index e25d5d84..f11e861d 100644 --- a/Tests/macSCPCoreTests/TunnelRuntimeFactoryWiringTests.swift +++ b/Tests/macSCPCoreTests/TunnelRuntimeFactoryWiringTests.swift @@ -18,7 +18,7 @@ import Testing /// listeners on port 0, a connection double standing in for the SSH server /// and refusing every channel, no rig. The SSH half is measured against the /// rig in `TunnelRigITests.aFailedConnectionIsCountedWhileTheForwardStaysUp`. -@Suite("Tunnel runtime factory wiring", .timeLimit(.minutes(1))) +@Suite("Tunnel runtime factory wiring", .timeLimit(.minutes(5))) struct TunnelRuntimeFactoryWiringTests { /// End to end through the runner: a `-L` whose server refuses the diff --git a/Tests/macSCPCoreTests/TunnelRuntimeTests.swift b/Tests/macSCPCoreTests/TunnelRuntimeTests.swift index bca537f0..dd7435e2 100644 --- a/Tests/macSCPCoreTests/TunnelRuntimeTests.swift +++ b/Tests/macSCPCoreTests/TunnelRuntimeTests.swift @@ -19,7 +19,7 @@ import Testing /// Written after a mutation probe on the cancellation gate came back GREEN /// (2026-09-06): nothing reached this type, because a `.remote` runtime is /// otherwise only built over a live SSH connection. -@Suite("Tunnel runtime", .timeLimit(.minutes(1))) +@Suite("Tunnel runtime", .timeLimit(.minutes(5))) struct TunnelRuntimeTests { @Test func aForwardThatEndsAfterTheServerConfirmedItIsReported() async throws { diff --git a/Tests/macSCPCoreTests/WebDAVFileSystemIntegrationTests.swift b/Tests/macSCPCoreTests/WebDAVFileSystemIntegrationTests.swift index d77b88ee..f2cdbd56 100644 --- a/Tests/macSCPCoreTests/WebDAVFileSystemIntegrationTests.swift +++ b/Tests/macSCPCoreTests/WebDAVFileSystemIntegrationTests.swift @@ -23,7 +23,7 @@ import Testing "WebDAVFileSystem against Docker Apache", .enabled(if: ProcessInfo.processInfo.environment["MACSCP_ITEST"] == "1"), .serialized, - .timeLimit(.minutes(1)) + .timeLimit(.minutes(5)) ) struct WebDAVFileSystemIntegrationTests { diff --git a/Tests/macSCPCoreTests/WebDAVFileSystemWriteTests.swift b/Tests/macSCPCoreTests/WebDAVFileSystemWriteTests.swift index 5b47bcba..11ebfe57 100644 --- a/Tests/macSCPCoreTests/WebDAVFileSystemWriteTests.swift +++ b/Tests/macSCPCoreTests/WebDAVFileSystemWriteTests.swift @@ -95,7 +95,7 @@ private struct WriteOutcome: Sendable { /// `.timeLimit` is the one clock these tests carry: the three `abandonable` /// waits below end through the cancellation it performs, never through a /// timer of their own. -@Suite("WebDAVFileSystem writes", .timeLimit(.minutes(1))) +@Suite("WebDAVFileSystem writes", .timeLimit(.minutes(5))) struct WebDAVFileSystemWriteTests { private let config = WebDAVConnectionConfig( baseURL: "https://dav.example.com/dav", username: "u", useNextcloudPath: false, From 7b9ec3347812dc01769e7b3025aecdf99713a214 Mon Sep 17 00:00:00 2001 From: noidee Date: Fri, 30 Oct 2026 12:01:26 +0100 Subject: [PATCH 6/8] test: a control of one second is not resolution for a three-second bar Run 41040277938 (`c8041d19`) took the gate from 73 red tests to one, and the reason tally is now empty: gh run view 41040277938 --repo NoiXdev/macSCP --log grep -c 'Time limit was exceeded' # 0, against 148 a run earlier at a run duration of 129.852 s against the previous run's 131.530 s -- so the bounds held on a runner that was just as slow, which is what the previous commit claimed and had not yet shown. The one test left is this suite's own timing half, and it failed on a run with nothing blocking: test 2 printed liveness: test2 ambient=1.004113042 seconds gap=21.006908125 seconds ceiling=3.012339126 seconds and asserted on the gap, because the gate `a64df8ee` added was `ambient <= bar`. `bar` is derived FROM `ambient` (`min(max(ambient * 3, 2 s), 4 s)`), so that condition reduces to `ambient > 4 s` and nothing else -- a control of one second passed it and was treated as adequate resolution for a three-second bar. It is not. The control and the gap are sampled at different times, this runner's noise arrives in bursts, and one quiet 600 ms window says nothing about the next six seconds. Six healthy samples now exist, three runs, no block present in any of them: 40965600726 test 1 ambient 23.317 gap 4.924 ceiling 4.0 40965600726 test 2 ambient 13.707 gap 0.449 ceiling 4.0 41030496532 test 1 ambient 5.952 gap 17.047 ceiling 4.0 41030496532 test 2 ambient 0.910 gap 0.180 ceiling 2.730 41040277938 test 1 ambient 5.697 gap 8.925 ceiling 4.0 41040277938 test 2 ambient 1.004 gap 21.007 ceiling 3.012 The gaps span 0.180 s to 21.007 s with nothing under test, a factor of 117, against a signal of 5 s in test 1 and 6 s in test 2. No bound both detects a 6 s block and survives 21 s of noise. Test 1's control was over the 4 s cap in all three runs, so its timing half has never once been in a position to give a verdict on this image; the single run where the old gate did assert produced a false red. So the gate becomes a resolution floor in absolute terms: `ambient <= .milliseconds(500)`, and the gap is reported rather than asserted above it. The figure is the gap between two measured populations rather than a knob -- 0.030269209 s and 0.030088625 s on this development machine, against the six CI samples above whose smallest is 0.909875959 s. Half a second is an order of magnitude above the first population, below all of the second, and leaves the smallest block this suite plants ten times the control it must be told apart from. What this does NOT do is recover coverage of a caller-side blocking bridge on CI. That shape produces a gap of the dial's whole duration and the timing half is the only thing that catches it; above the floor it is unproven, and this file already said so where it describes what the skip leaves out. Nothing here makes that better -- the measurement says the instrument has no resolution on that runner, and a bound asserted without resolution buys a false red, not coverage. One alternative considered and rejected: bracket the window with a second ambient sample afterwards and take the larger of the two. It needs evidence that a trailing sample catches a burst the leading one missed, and run 41040277938 took no trailing sample, so adopting it would be a guess wearing a measurement's clothes. The gate is only worth changing if it still catches what it is for, so the plant that proves the assertion fires was re-run against it rather than reasoned about: scripts/mutation-probe \ --filter awaitingTheConnectorDoesNotKeepTheMainActor \ --apply <5 s blockThisThread before the awaited connect> RESULT: RED -- 1 test ran, caught by awaitingTheConnectorDoesNotKeepTheMainActor(). The control on this machine is 0.03 s, an order of magnitude under the new floor, so the assertion is reached here exactly as before. Plant D, which lifts the control to ~5 s, was NOT re-run: ~5 s clears the old 4 s cap and the new 500 ms floor alike, so no measurement there separates the two gates, and a re-run would have been a green with nothing to attribute it to. Also corrected, a claim about other code that was wrong in `a64df8ee`'s probe record: it said "test 2 is gated behind `MACSCP_ITEST=1`". It is not -- test 2 carries `.enabled(if: StalledLoopbackEndpoint.isAvailable)`, which is why it skips here and runs on CI, and why the probes could only ever have covered test 1. Co-Authored-By: Claude Opus 5 --- .../ConnectMainActorLivenessTests.swift | 99 +++++++++++++++---- 1 file changed, 80 insertions(+), 19 deletions(-) diff --git a/Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift b/Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift index 3e9e4751..196e55f5 100644 --- a/Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift +++ b/Tests/macSCPCoreTests/ConnectMainActorLivenessTests.swift @@ -233,23 +233,65 @@ struct ConnectMainActorLivenessTests { min(max(ambient * 3, .seconds(2)), .seconds(4)) } + /// The ambient control a run must come in under before its gap is + /// asserted on at all. Not a tuning knob: the figure is the gap between + /// two measured populations. On this development machine the control + /// reads 0.030269209 s, 0.030088625 s and 0.0310155 s (three runs, + /// 2026-10-30); across + /// three CI runs on `macos-latest` its six samples are 23.317136667, + /// 13.706875625, 5.95172925, 0.909875959, 5.696799124999999 and + /// 1.004113042 s. Half a second sits an order of magnitude above the + /// first population and below all of the second, and leaves a 5 s block + /// — the smallest this suite plants — ten times the control it would + /// have to be distinguished from. + static let resolutionFloor = Duration.milliseconds(500) + /// States a measured gap against the ceiling — and declines to assert - /// when the control says the instrument cannot separate the two - /// magnitudes at all. + /// unless the run has shown the instrument has the resolution to + /// separate a block from its own scheduling noise. /// - /// `ceiling(forAmbient:)` is capped at four seconds, so an ambient + /// This gate read `ambient <= bar` until 2026-10-30, explained as + /// "`ceiling(forAmbient:)` is capped at four seconds, so an ambient /// control ABOVE that cap describes a run in which the noise alone - /// already clears the bar a block would have to clear. There is no - /// verdict to give there: a gap under the ceiling is unreachable, and a - /// gap over it is what noise and block both produce. CI run - /// 40965600726 (2026-10-29, `79851463`, `macos-latest`) is the first - /// run on record in that regime — it printed - /// `ambient=23.317136667 seconds gap=4.923961375 seconds ceiling=4.0 - /// seconds` and failed test 1 on the gap. The ambient control is 4.74x - /// the gap it red-flagged, which is the whole of the argument that the - /// gap was the harness; the run is read with + /// already clears the bar a block would have to clear" — the withdrawn + /// wording and the withdrawn condition. `bar` is derived FROM `ambient`, + /// so that test reduced to `ambient > 4 s`, and it let a control of one + /// second stand as adequate resolution for a three-second ceiling. + /// Run 41040277938 (2026-10-30, `c8041d19`) is where that broke: test 2 + /// printed `ambient=1.004113042 seconds gap=21.006908125 seconds + /// ceiling=3.012339126 seconds` and FAILED on the gap. Nothing was + /// blocking — the run's other 7974 tests passed. One sample of 1 s does + /// not bound the next six seconds of a runner whose noise arrives in + /// bursts, and the control and the gap are drawn at different times, so + /// a low control is not evidence of a quiet window. + /// + /// Six healthy samples over three runs, every one of them with no block + /// present, read with + /// + /// gh run view --repo NoiXdev/macSCP --log + /// + /// 40965600726 test 1 ambient 23.317 gap 4.924 ceiling 4.0 + /// 40965600726 test 2 ambient 13.707 gap 0.449 ceiling 4.0 + /// 41030496532 test 1 ambient 5.952 gap 17.047 ceiling 4.0 + /// 41030496532 test 2 ambient 0.910 gap 0.180 ceiling 2.730 + /// 41040277938 test 1 ambient 5.697 gap 8.925 ceiling 4.0 + /// 41040277938 test 2 ambient 1.004 gap 21.007 ceiling 3.012 /// - /// gh run view 40965600726 --repo NoiXdev/macSCP --log + /// The gaps span 0.180 s to 21.007 s with nothing under test — a factor + /// of 117 — against a signal of 5 s in test 1 and 6 s in test 2. Test + /// 1's control was over the 4 s cap in all three runs, so its timing + /// half has never once been in a position to give a verdict on this + /// image; the one time the old gate did assert, it was wrong. A bound + /// that both detects a 6 s block and survives 21 s of noise does not + /// exist, which is why this is a resolution gate and not a wider + /// ceiling. + /// + /// One alternative was considered and rejected for want of evidence: + /// bracketing the window with a second ambient sample afterwards and + /// taking the larger. It would have to be shown that the trailing + /// sample catches a burst the leading one missed, and run 41040277938 + /// measured no trailing sample, so adopting it would be a guess wearing + /// a measurement's clothes. /// /// Reported, then, rather than asserted on, following the house pattern /// of `FileDiffCostTests.report(_:)`: `print` puts the figures in the @@ -295,8 +337,26 @@ struct ConnectMainActorLivenessTests { /// well and proves nothing either way — it never touches the dial /// window the gap is measured over, so the assertion was never asked /// to see it. Both probes ran `--filter - /// awaitingTheConnectorDoesNotKeepTheMainActor`; test 2 is gated behind - /// `MACSCP_ITEST=1` and was not probed. + /// awaitingTheConnectorDoesNotKeepTheMainActor`; test 2 was not probed. + /// Both probes ran on this development machine, where the control reads + /// 0.03 s and so clears `resolutionFloor`, so the gate change above is + /// not expected to move either outcome, and plant A was re-run against + /// the new gate to check that rather than assume it. Measured + /// 2026-10-30, with + /// + /// scripts/mutation-probe --filter \ + /// awaitingTheConnectorDoesNotKeepTheMainActor --apply + /// + /// RESULT: RED — 1 test ran and the plant was caught by + /// `awaitingTheConnectorDoesNotKeepTheMainActor()`. Plant D was not + /// re-run: its control of ~5 s is above the old 4 s cap and the new + /// 500 ms floor alike, so nothing measured there would tell the two + /// gates apart. + /// + /// Corrected 2026-10-30: this paragraph said "test 2 is gated behind + /// `MACSCP_ITEST=1`". It is not — it carries + /// `.enabled(if: StalledLoopbackEndpoint.isAvailable)`, which is why it + /// is skipped here and runs on CI. @MainActor private func stateGapAgainstCeiling( _ log: TickLog, @@ -307,12 +367,13 @@ struct ConnectMainActorLivenessTests { let bar = ceiling(forAmbient: ambient) let figures = "\(test) ambient=\(ambient) gap=\(log.largestGap) ceiling=\(bar)" print("liveness: " + figures) - guard ambient <= bar else { + guard ambient <= Self.resolutionFloor else { let text = "liveness: \(figures) — NOT ASSERTED. The ambient control is " - + "above the ceiling, so noise and block are not separable in " - + "this run and the gap has no verdict to give. Measured over " - + "\(log.stamps.count) ticks." + + "above \(Self.resolutionFloor), so this run has not shown " + + "that the instrument can tell a block from its own " + + "scheduling noise, and the gap has no verdict to give. " + + "Measured over \(log.stamps.count) ticks." print(text) Issue.record( Comment(rawValue: text), severity: .warning, From b9ba9d27033d56bd20ee85676142f009f15a8e8e Mon Sep 17 00:00:00 2001 From: noidee Date: Fri, 30 Oct 2026 12:24:03 +0100 Subject: [PATCH 7/8] fix(test): the catalogue-coverage test reads a main-actor property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 41045404989 (`7b9ec334`) is the first run of the gate on `macos-latest` whose test step passed: `Test run with 7975 tests in 650 suites passed after 137.078 seconds`, the step taking 581 s of its 960 s budget. That let the Warning gate execute for the first time -- it had come back `skipped` on four consecutive runs because the tests were red -- and it failed on its first outing: Unique warning locations: 1 (budget: 0) ----- warnings ----- …/Tests/macSCPAppKitTests/EmptyContainerListTests.swift:131:50: warning: main actor-isolated static property 'allLines' can not be referenced from a nonisolated context `everyKeyTheViewUsesIsOneTheCatalogueChecksCover` reads `EmptyContainerListView.allLines` from a nonisolated test. Both siblings that touch the same property -- `everyLineResolvesInAllFourLocales` and `theEnglishCatalogueSaysTheDefault` -- carry `@MainActor @Test`, so the fix is the file's own idiom and not a new convention: this one test was missing it. The warning is the TOOLCHAIN's, not this branch's. Counted across the saved job logs with grep -c 'EmptyContainerListTests.swift:131' it appears 25 times in each of runs 40965600726, 41030496532 and 41040277938 -- every run on `macos-latest` -- and 26 in 41045404989, the extra line being the gate quoting it. In run 38225383495, the last green run on `macos-15` (Xcode 16.4, Swift 6.1.2), it appears 0 times. Swift 6.3.3 diagnoses what 6.1.2 did not, which is the kind of thing moving the image was meant to surface; nothing in `Tests/` was edited at that site by any commit on this branch. Why the gate's first red was a surprise rather than a prediction, which is worth one line because the lesson is about the check and not the code: this warning was reported earlier in the session as absent, from a search that stripped the `gh run view --log` prefix with `sed -E 's/^[a-z]*\t[^[:space:]]*...'` written as `[^\t]*` for the step-name field. Outside a bracket expression `\t` is a tab; INSIDE one it is a backslash and the letter `t`, so the class excluded `t` and could not match a step named `Unit-Tests`. Nothing was stripped, the following `^`-anchored grep matched nothing, and nothing-matched read as no-warnings. A negative check with no positive control beside it, which CLAUDE.md has a section about. Verified with the gate's own pattern, this time with a positive check beside the negative one. After the fix, `swift build --build-tests` with that file touched so it recompiles: grep -cE '^[^[:space:]]+:[0-9]+:[0-9]+: warning:' # 0 and the same pattern against the CI warning line, stripped of its log prefix, returns 1 -- so the pattern does match this class of line rather than nothing at all. Run over the whole of run 41040277938's log with the corrected strip it reports 1 unique location, which is the figure the gate itself printed on run 41045404989. The method now reproduces the authority it was meant to predict. The local build does emit other warnings, all of them from `macro expansion #expect:` under this machine's Swift 6.4 and absent on CI's 6.3.3. None are counted by the gate: its pattern anchors a path at the line start, and `macro expansion #expect:1:46: warning:` has a space inside the first field. Co-Authored-By: Claude Opus 5 --- Tests/macSCPAppKitTests/EmptyContainerListTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/macSCPAppKitTests/EmptyContainerListTests.swift b/Tests/macSCPAppKitTests/EmptyContainerListTests.swift index e2945e90..fc107fde 100644 --- a/Tests/macSCPAppKitTests/EmptyContainerListTests.swift +++ b/Tests/macSCPAppKitTests/EmptyContainerListTests.swift @@ -121,7 +121,7 @@ struct EmptyContainerListTests { /// a key added to the view and not to `allLines` is red rather than /// unmeasured. Read from comment-free source, so the list in the doc /// comment cannot stand in for the code. - @Test func everyKeyTheViewUsesIsOneTheCatalogueChecksCover() throws { + @MainActor @Test func everyKeyTheViewUsesIsOneTheCatalogueChecksCover() throws { let source = try SourceCorpus.commentFree( of: SourceCorpus.url(of: .sources) .appendingPathComponent("MacSCPAppKit/EmptyContainerListView.swift")) From 6f1fe42a24c2cf9a98eae6f3268237d131d120f0 Mon Sep 17 00:00:00 2001 From: noidee Date: Fri, 30 Oct 2026 12:28:11 +0100 Subject: [PATCH 8/8] docs: a pattern that matches nothing is an attestation, not a check The rule under "Guards that name what they watch" -- a negative check needs a positive check beside it -- was written about checks that live in the suite. On 2026-10-30 it was broken by a check run once at a keyboard, and the consequence was a wrong figure reported as measured, so the rule now says it binds both. The case: the CI warning gate's own pattern was run against a saved `gh run view --log` to predict whether `MAX_WARNINGS=0` would pass. That needs the log's `\t\t ` prefix stripped, and the strip was written `[^\t]*` for the step-name field. Outside a bracket expression `\t` is a tab; inside one it is a backslash and the letter `t`, so the class excluded `t` and could not cross `Unit-Tests`. Nothing was stripped, the `^`-anchored grep that followed matched nothing, and nothing-matched was read as no-warnings -- while the log held the warning 25 times, and the gate went on to fail on it on run 41045404989. The two commands in the new paragraph differ in that one bracket expression; the first leaves the line untouched, the second prints `STRIPPED`. Both were run in the form they are committed, after the paragraph was written, and print what it says they print. Same family as the `grep`/ugrep `$` case already recorded under point 3, and the same remedy, which is why this sits beside point 1 rather than starting a section: before believing a zero, feed the pattern something it MUST match and watch it match. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index be09dc07..e153a9d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -483,6 +483,29 @@ own: the thing it scans is there at all. Without one it is not a guard, it is a comment that runs. + **This binds a check run once at a keyboard exactly as it binds one in + the suite**, measured 2026-10-30 on the CI warning gate. The gate's own + pattern was run against a saved `gh run view --log` to predict whether + `MAX_WARNINGS=0` would pass, which needs the log's + `\t\t ` prefix stripped first, and the strip was + written `sed -E 's/^[a-z]*\t[^\t]*\t[0-9T:.Z-]+ //'`. Outside a bracket + expression `\t` is a tab; INSIDE one it is a backslash and the letter + `t`, so `[^\t]*` excluded `t` and could not cross a step named + `Unit-Tests`. Nothing was stripped, the following `^`-anchored grep + matched nothing, and nothing-matched was reported as no-warnings. The + log held the warning 25 times. One line separates the two readings, and + it is the positive control this rule asks for: + + printf 'x\tUnit\tY\n' | sed -E 's/^x\t[^\t]*\tY$/STRIPPED/' + printf 'x\tUnit\tY\n' | sed -E 's/^x\t[^[:space:]]*\tY$/STRIPPED/' + + The first prints the line unchanged, the second prints `STRIPPED`. Same + family as the `grep`/ugrep `$` case under point 3 below — a pattern that + silently matches nothing turns a check into an attestation — and the + same remedy: before believing a zero, feed the pattern something it MUST + match and watch it match. The corrected strip reproduces the figure the + gate itself printed, 1 unique location, where the original reported 0. + 2. **A guard that spells a symbol it could read instead is waiting for a rename.** Walk the name back from a call site, derive a catalogue key from the type that owns it. A literal is a second copy of a name, and