Skip to content

Merge plan PRs when GitHub auto-merge is off or there is no CI - #3280

Merged
integry merged 5 commits into
mainfrom
fix/auto-merge-without-github-automerge
Oct 20, 2026
Merged

integry merged 5 commits into
mainfrom
fix/auto-merge-without-github-automerge

Conversation

@integry

@integry integry commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Problem

Epic PR integry/agent-tank-website#43 reached its Ultrafix goal (8/10, no findings) with auto-merge selected in the plan, but never merged and the epic stalled:

  1. GitHub refused to arm auto-merge: Auto merge is not allowed for this repository. ProPR only logged a warning.
  2. The direct-merge fallback runs on check events, but the repo's CI runs only for PRs into main, so this epic sub-PR had no checks and no event ever fired.
  3. The direct merge never treated a commit with no CI signal as mergeable.

The same arming failure hit integry/propr-fleet#153.

Fix

  • enableAutoMerge reports unavailable when GitHub cannot arm auto-merge at all: the repo setting is off, or the PR is already in clean status.
  • Ultrafix finish: an unavailable arm falls back to the direct merge path (reevaluatePRAutoMerge) with all its gates (labels, Ultrafix state, CI, mergeability, repository policy, active work). Only the finishing job's own task/job is ignored in the active-work check.
  • Setting suggestion: when the repo disallows auto-merge, ProPR comments on the PR suggesting Settings → General → Allow auto-merge, both on first PR arming and after Ultrafix.
  • CI readiness (getCiMergeReadiness): merges when blocking checks have finished passing, or when there is no CI at all for a head older than 2 minutes. The grace period keeps checks that haven't registered yet from being mistaken for missing CI. Pending or failing checks and API errors still block. Closed PRs are skipped.
  • Daemon sweep: every minute, open plan PRs (linked, not merged/closed, updated in the last 14 days) go through the same gates. A repo without CI, or CI that finished before Ultrafix did, produces no later event that would merge the PR. There are currently 2 candidates, so the cost is a few GitHub calls per minute.

Tests

  • New: test/ultrafixAutoMergeFallback.test.ts, test/autoMergeSweep.test.ts, getCiMergeReadiness cases in test/checkRunHandler.test.ts, and the unavailable case in test/autoMergeOperations.test.ts.
  • Related suites pass, including checkRunHandler, the Ultrafix suites, taskIntentWithdrawal, the issueJob suites, daemon and webhook. Some Ultrafix test mocks gained the new core export.
  • test/intentWithdrawalWebhook.test.ts fails 4 tests on main as well; that is unrelated.

🤖 Generated with Claude Code

…e is no CI

Epic PR integry/agent-tank-website#43 reached its Ultrafix goal but never
merged. GitHub refused to arm auto-merge ("Auto merge is not allowed for this
repository"), the failure was only logged, and the direct-merge fallback never
ran: it is driven by check events, the PR targets an epic branch the repo's CI
does not run on, and a commit with no CI signal was never treated as mergeable.

- enableAutoMerge reports `unavailable` when GitHub cannot arm auto-merge at all
  (repository setting off, or the PR is already in clean status).
- After Ultrafix reaches its goal, an unavailable arm falls back to the direct
  merge path with all its gates, ignoring only the finishing job's own work.
- When the repository setting is off, ProPR comments on the PR suggesting
  "Allow auto-merge" (Ultrafix finish and first PR arming).
- The direct merge accepts CI that has finished passing, or no CI at all for a
  head that is older than a 2-minute grace, so unregistered checks are not
  mistaken for missing CI. Closed PRs are skipped.
- The daemon sweeps open plan PRs every minute through the same gates, since a
  repository without CI, or CI that finished before Ultrafix did, produces no
  further event that would merge the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@propr-dev

propr-dev Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

CI failed: Full Test Suite Shard 1/4

Please investigate and fix this CI failure.

  • Check: Full Test Suite Shard 1/4
  • Result: failure
  • Commit: aafe28a0169d (aafe28a0169d6b9ccc9a3bc961ba21b42ead4190)
  • Details: View CI failure

Failure evidence

.github:11
Process completed with exit code 1.

.github:39412
Process completed with exit code 1.

.github:2
Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: actions/checkout@11d5960. For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/

@propr-dev

propr-dev Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

CI failed: Run Full Test Suite

Please investigate and fix this CI failure.

Failure evidence

.github:69
Process completed with exit code 1.

.github:66
Full test suite shards finished with result 'failure'.

.github:4
Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: actions/checkout@11d5960, actions/download-artifact@018cc2c. For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/

@github-actions

Copy link
Copy Markdown

Full Test Suite Results

Validation failed during: Test shard (shard 1).

  • Docs site validation: skipped (classifier decision: false)
  • Shard coverage verification: success
  • Hosted native Electron units: skipped (classifier decision: false)
  • Shard 1/4: failed during Test shard in 870.6s on GitHub Actions 1000100923
    • test/ultrafixRecoveryRedisIntegration.test.ts: exit 1
  • Shard 2/4: passed in 788.8s on GitHub Actions 1000100922
  • Shard 3/4: passed in 500.2s on GitHub Actions 1000100926
  • Shard 4/4: passed in 754.6s on GitHub Actions 1000100929
View shard 1/4 output
...(truncated)

ctly
        ok 1 - adapter reshapes NX/EX options correctly
          ---
          duration_ms: 1.581646
          type: 'test'
          ...
        # Subtest: adapter passes through plain set without options
        ok 2 - adapter passes through plain set without options
          ---
          duration_ms: 0.714672
          type: 'test'
          ...
        # Subtest: adapter returns null when NX key already exists
        ok 3 - adapter returns null when NX key already exists
          ---
          duration_ms: 0.650977
          type: 'test'
          ...
        1..3
    ok 11 - Redis adapter shape (server.ts integration path)
      ---
      duration_ms: 3.087321
      type: 'suite'
      ...
    1..11
ok 1 - Webhook Replay Protection
  ---
  duration_ms: 91.340542
  type: 'suite'
  ...
# [2026-10-20 00:42:54.759 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-BKQZkJ/225-webhookReplayProtection.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-10-20 00:42:54.845 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
1..1
# tests 22
# suites 12
# pass 22
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 1830.664173
[225/230] passed test/webhookReplayProtection.test.ts in 1.9s

[226/230] test/worker.integration.test.ts
TAP version 13
# Subtest: Worker Integration - Concurrent Model Execution
    # Subtest: concurrent jobs with different models create unique worktrees
    ok 1 - concurrent jobs with different models create unique worktrees
      ---
      duration_ms: 8.085303
      type: 'test'
      ...
    # Subtest: delay function prevents exact simultaneous execution
    ok 2 - delay function prevents exact simultaneous execution
      ---
      duration_ms: 280.711356
      type: 'test'
      ...
    # Subtest: worktree cleanup after concurrent execution
    ok 3 - worktree cleanup after concurrent execution
      ---
      duration_ms: 3.399901
      type: 'test'
      ...
    # Subtest: file system isolation between concurrent workers
    ok 4 - file system isolation between concurrent workers
      ---
      duration_ms: 3.53524
      type: 'test'
      ...
    # Subtest: error handling in concurrent execution
    ok 5 - error handling in concurrent execution
      ---
      duration_ms: 74.187848
      type: 'test'
      ...
    1..5
ok 1 - Worker Integration - Concurrent Model Execution
  ---
  duration_ms: 371.055408
  type: 'suite'
  ...
1..1
# tests 5
# suites 1
# pass 5
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 606.869199
[226/230] passed test/worker.integration.test.ts in 0.7s

[227/230] test/workerConcurrency.test.ts
TAP version 13
# Subtest: Hosted ignores absent, lower, higher and malformed persisted concurrency at every tier
ok 1 - Hosted ignores absent, lower, higher and malformed persisted concurrency at every tier
  ---
  duration_ms: 0.990807
  type: 'test'
  ...
# Subtest: CONFIG_REPO preserves valid persisted overrides and falls back consistently
ok 2 - CONFIG_REPO preserves valid persisted overrides and falls back consistently
  ---
  duration_ms: 0.275476
  type: 'test'
  ...
# Subtest: invalid environment values use default 5; partial numbers and fractions are never truncated
ok 3 - invalid environment values use default 5; partial numbers and fractions are never truncated
  ---
  duration_ms: 0.296025
  type: 'test'
  ...
1..3
# tests 3
# suites 0
# pass 3
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 199.652457
[227/230] passed test/workerConcurrency.test.ts in 0.3s

[228/230] test/workerStateManagerRedisIntegration.test.ts
TAP version 13
# Subtest: Redis CAS rejects a stale metadata snapshot after terminalization
ok 1 - Redis CAS rejects a stale metadata snapshot after terminalization
  ---
  duration_ms: 20.131205
  type: 'test'
  ...
# [2026-10-20 00:42:56.738 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-BKQZkJ/228-workerStateManagerRedisIntegration.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-10-20 00:42:56.753 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
1..1
# tests 1
# suites 0
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 662.059918
[228/230] passed test/workerStateManagerRedisIntegration.test.ts in 0.8s

[229/230] test/workflowPreviewPublication.test.ts
TAP version 13
# (node:33921) ExperimentalWarning: Module mocking is an experimental feature and might change at any time
# (Use `node --trace-warnings ...` to show where the warning was created)
# Subtest: an issue run publishes no preview evidence when its workflow disables every preview type
ok 1 - an issue run publishes no preview evidence when its workflow disables every preview type
  ---
  duration_ms: 365.093378
  type: 'test'
  ...
# Subtest: an issue run without a workflow restriction still prepares the same preview artifact
ok 2 - an issue run without a workflow restriction still prepares the same preview artifact
  ---
  duration_ms: 360.11756
  type: 'test'
  ...
# Subtest: a PR follow-up publishes no preview evidence when its workflow disables every preview type
ok 3 - a PR follow-up publishes no preview evidence when its workflow disables every preview type
  ---
  duration_ms: 376.755897
  type: 'test'
  ...
# Subtest: a PR follow-up without a workflow restriction still prepares the same preview artifact
ok 4 - a PR follow-up without a workflow restriction still prepares the same preview artifact
  ---
  duration_ms: 551.747432
  type: 'test'
  ...
1..4
# tests 4
# suites 0
# pass 4
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2672.192319
[229/230] passed test/workflowPreviewPublication.test.ts in 2.8s

[230/230] propr-ui#1/4 (workspace test script)

> propr-ui@0.0.1 test
> vitest run --shard=1/4


�[1m�[30m�[46m RUN �[49m�[39m�[22m �[36mv5.0.0 �[39m�[90m/home/runner/work/propr/propr/propr-ui�[39m

�[90mstderr�[2m | src/pages/InboxPage.test.tsx�[2m > �[22m�[2mInbox page�[2m > �[22m�[2mexpands a system notification in place instead of navigating
�[22m�[39mAn update to InboxPage inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/components/Agents/AgentEditor.test.tsx�[2m > �[22m�[2mAgentEditor�[2m > �[22m�[2mpreviews the next run of a preset in UTC
�[22m�[39mAn update to AgentScopeSection inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentEditor inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentEditor inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

 �[32m✓�[39m src/components/Agents/AgentEditor.test.tsx �[2m(�[22m�[2m25 tests�[22m�[2m)�[22m�[33m 2621�[2mms�[22m�[39m
   �[32m✓�[39m AgentEditor �[2m(25)�[22m
     �[33m�[2m✓�[22m�[39m shows the shared validation message for an invalid cron and sends no request�[33m 441�[2mms�[22m�[39m
 �[32m✓�[39m src/pages/InboxPage.test.tsx �[2m(�[22m�[2m43 tests�[22m�[2m)�[22m�[33m 3697�[2mms�[22m�[39m
   �[32m✓�[39m Inbox page �[2m(43)�[22m
     �[32m✓�[39m offline �[2m(5)�[22m
       �[33m�[2m✓�[22m�[39m a first visit while offline shows the offline state, then loads once on reconnect�[33m 423�[2mms�[22m�[39m
 �[32m✓�[39m src/desktop/DesktopExperience.test.tsx �[2m(�[22m�[2m22 tests�[22m�[2m)�[22m�[33m 1199�[2mms�[22m�[39m
 �[32m✓�[39m src/pages/AgentsPage.test.tsx �[2m(�[22m�[2m22 tests�[22m�[2m)�[22m�[33m 2014�[2mms�[22m�[39m
   �[32m✓�[39m AgentsPage �[2m(22)�[22m
     �[33m�[2m✓�[22m�[39m opens an agent beside the list on wide screens without leaving /automations�[33m 305�[2mms�[22m�[39m
     �[33m�[2m✓�[22m�[39m reads every page so agents past the first page can be found by search�[33m 421�[2mms�[22m�[39m
Not implemented: navigation to another Document
Not implemented: navigation to another Document
Not implemented: navigation to another Document
Not implemented: navigation to another Document
Not implemented: navigation to another Document
 �[32m✓�[39m src/components/TaskList/TaskRows.test.tsx �[2m(�[22m�[2m20 tests�[22m�[2m)�[22m�[33m 828�[2mms�[22m�[39m
 �[32m✓�[39m src/hooks/useVoicePreference.test.tsx �[2m(�[22m�[2m20 tests�[22m�[2m)�[22m�[33m 340�[2mms�[22m�[39m
�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar follow-up CI cancellation�[2m > �[22m�[2mrenders the option off by default with helper text about restarting checks
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar follow-up CI cancellation�[2m > �[22m�[2masks for a selection and discloses the instance fallback while the enabled option selects nothing
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar follow-up CI cancellation�[2m > �[22m�[2masks for a selection and discloses the instance fallback while the enabled option selects nothing
�[22m�[39mAn update to CancelCiDuringFollowupControl inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar follow-up CI cancellation�[2m > �[22m�[2mshows the selected workflows and reports an edited selection as exact identities
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar follow-up CI cancellation�[2m > �[22m�[2mdoes not report a selection that did not change
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar follow-up CI cancellation�[2m > �[22m�[2mpreserves comma-containing names on unchanged blur and when editing another selection
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar follow-up CI cancellation�[2m > �[22m�[2mdoes not save incomplete quoted input
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar follow-up CI cancellation�[2m > �[22m�[2mreflects the stored value and reports a toggle
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar follow-up CI cancellation�[2m > �[22m�[2mreflects the stored value and reports a toggle
�[22m�[39mAn update to CancelCiDuringFollowupControl inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mrecognizes and deselects a padded display name with triggers [ 'pull_request' ]
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mrecognizes and deselects a padded display name with triggers [ 'push' ]
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mrecognizes and deselects a padded display name with triggers null
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mpreserves a normalized display name when removing unavailable selections after discovery
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2moffers the repository workflows and matches stored entries by any identity
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mallows manual identities outside a cached listing and keeps checkbox edits in sync
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mallows manual identities outside a cached listing and keeps checkbox edits in sync
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstdout�[2m | src/hooks/useHeaderStats.recovery.test.tsx�[2m > �[22m�[2museHeaderStats live recovery�[2m > �[22m�[2mcommits untouched initial resources when a partial live refresh supersedes queue
�[22m�[39m[useHeaderStats] Received changed queue stats, scheduling stats refresh

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mpreserves manual input across a successful populated discovery without saving it
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mpreserves manual input across a successful empty discovery without saving it
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mstores a picked workflow by file name and removes every spelling of an unpicked one
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mflags stored entries that match no workflow in the repository
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mflags a stored selection only after an empty listing successfully loads
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mdoes not flag an empty selection after an empty listing successfully loads
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mfalls back to typing workflows when GitHub cannot list them
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar detected workflows�[2m > �[22m�[2mdoes not ask GitHub for workflows while the option is off
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar non-blocking checks�[2m > �[22m�[2mshows the stored checks and saves an edited list on blur
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

�[90mstderr�[2m | src/components/RepositorySettingsBar.test.tsx�[2m > �[22m�[2mRepositorySettingsBar non-blocking checks�[2m > �[22m�[2mdoes not save unchanged or malformed input
�[22m�[39mEncountered two children with the same key, `repo-1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

 �[32m✓�[39m src/components/RepositorySettingsBar.test.tsx �[2m(�[22m�[2m25 tests�[22m�[2m)�[22m�[33m 1273�[2mms�[22m�[39m
�[90mstdout�[2m | src/hooks/useHeaderStats.recovery.test.tsx�[2m > �[22m�[2museHeaderStats live recovery�[2m > �[22m�[2mreconciles a successful completion to zero from the queue subscription
�[22m�[39m[useHeaderStats] Received changed queue stats, scheduling stats refresh

�[90mstdout�[2m | src/hooks/useHeaderStats.recovery.test.tsx�[2m > �[22m�[2museHeaderStats live recovery�[2m > �[22m�[2mrefreshes only queue activity and bounds identical periodic invalidations
�[22m�[39m[useHeaderStats] Received changed queue stats, scheduling stats refresh

�[90mstdout�[2m | src/hooks/useHeaderStats.recovery.test.tsx�[2m > �[22m�[2museHeaderStats live recovery�[2m > �[22m�[2mretries only a failed queue reconciliation and commits its fingerprint after recovery
�[22m�[39m[useHeaderStats] Received changed queue stats, scheduling stats refresh

�[90mstdout�[2m | src/hooks/useHeaderStats.recovery.test.tsx�[2m > �[22m�[2museHeaderStats live recovery�[2m > �[22m�[2mdefers hidden-tab churn and performs one full visible recovery
�[22m�[39m[useHeaderStats] Received changed queue stats, scheduling stats refresh

�[90mstdout�[2m | src/hooks/useHeaderStats.recovery.test.tsx�[2m > �[22m�[2museHeaderStats live recovery�[2m > �[22m�[2mrevalidates missed same-count draft and health changes after a web reconnect
�[22m�[39m[useHeaderStats] Received changed queue stats, scheduling stats refresh

�[90mstdout�[2m | src/hooks/useHeaderStats.recovery.test.tsx�[2m > �[22m�[2museHeaderStats live recovery�[2m > �[22m�[2mrevalidates missed same-count draft and health changes after a web reconnect
�[22m�[39m[useHeaderStats] Received changed queue stats, scheduling stats refresh

 �[32m✓�[39m src/hooks/useHeaderStats.recovery.test.tsx �[2m(�[22m�[2m11 tests�[22m�[2m)�[22m�[33m 1333�[2mms�[22m�[39m
   �[32m✓�[39m useHeaderStats live recovery �[2m(11)�[22m
     �[33m�[2m✓�[22m�[39m refreshes only queue activity and bounds identical periodic invalidations�[33m 306�[2mms�[22m�[39m
�[90mstderr�[2m | src/components/MobileBottomNavigation.test.tsx�[2m > �[22m�[2mMobileBottomNavigation�[2m > �[22m�[2mkeeps creation out of the More sheet
�[22m�[39mAn update to AgentTankSidebar inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/components/MobileBottomNavigation.test.tsx�[2m > �[22m�[2mMobileBottomNavigation�[2m > �[22m�[2mkeeps creation out of the More sheet
�[22m�[39mAn update to AgentTankSidebar inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/components/MobileBottomNavigation.test.tsx�[2m > �[22m�[2mMobileBottomNavigation�[2m > �[22m�[2mhides the MCP log without the settings permission
�[22m�[39mAn update to AgentTankSidebar inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/components/MobileBottomNavigation.test.tsx�[2m > �[22m�[2mMobileBottomNavigation�[2m > �[22m�[2mhides the MCP log without the settings permission
�[22m�[39mAn update to AgentTankSidebar inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/components/MobileBottomNavigation.test.tsx�[2m > �[22m�[2mMobileBottomNavigation�[2m > �[22m�[2mhides Access without member management permission
�[22m�[39mAn update to AgentTankSidebar inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/components/MobileBottomNavigation.test.tsx�[2m > �[22m�[2mMobileBottomNavigation�[2m > �[22m�[2mhides Access without member management permission
�[22m�[39mAn update to AgentTankSidebar inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/components/MobileBottomNavigation.test.tsx�[2m > �[22m�[2mMobileBottomNavigation�[2m > �[22m�[2msigns out from the identity section
�[22m�[39mAn update to AgentTankSidebar inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/components/MobileBottomNavigation.test.tsx�[2m > �[22m�[2mMobileBottomNavigation�[2m > �[22m�[2msigns out from the identity section
�[22m�[39mAn update to AgentTankSidebar inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

 �[32m✓�[39m src/components/MobileBottomNavigation.test.tsx �[2m(�[22m�[2m33 tests�[22m�[2m)�[22m�[33m 1731�[2mms�[22m�[39m
�[90mstderr�[2m | src/components/TaskPlanner/SetupWizard.test.tsx�[2m > �[22m�[2mSetupWizard�[2m > �[22m�[2mignores stale repo switches in edit mode when a newer selection finishes first
�[22m�[39mAn update to SetupWizard inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to SetupWizard inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to SetupWizard inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to SetupWizard inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

 �[32m✓�[39m src/components/TaskPlanner/SetupWizard.test.tsx �[2m(�[22m�[2m13 tests�[22m�[2m)�[22m�[32m 254�[2mms�[22m�[39m
 �[32m✓�[39m src/pages/TasksPage.split.test.tsx �[2m(�[22m�[2m14 tests�[22m�[2m)�[22m�[33m 1914�[2mms�[22m�[39m
   �[32m✓�[39m TasksPage split workspace �[2m(14)�[22m
     �[33m�[2m✓�[22m�[39m keeps the open task's runs and current state when a filter takes it off the list�[33m 508�[2mms�[22m�[39m
�[90mstdout�[2m | src/hooks/useCurrentUserBootstrap.test.tsx�[2m > �[22m�[2mdesktop current-user bootstrap�[2m > �[22m�[2mmounts an active scope at generation one and enables exactly one stable Manager after current validation
�[22m�[39m[SocketContext] Cleaning up socket connection

�[90mstdout�[2m | src/hooks/useCurrentUserBootstrap.test.tsx�[2m > �[22m�[2mdesktop current-user bootstrap�[2m > �[22m�[2moverlaps validation with demo-mode loading but keeps Manager disabled until mode resolves in StrictMode
�[22m�[39m[SocketContext] Cleaning up socket connection

�[90mstdout�[2m | src/hooks/useCurrentUserBootstrap.test.tsx�[2m > �[22m�[2mdesktop current-user bootstrap�[2m > �[22m�[2mconstructs once after activated validation and removes that Manager when revalidation fails
�[22m�[39m[SocketContext] Cleaning up socket connection

 �[32m✓�[39m src/hooks/useCurrentUserBootstrap.test.tsx �[2m(�[22m�[2m8 tests�[22m�[2m)�[22m�[32m 289�[2mms�[22m�[39m
 �[32m✓�[39m src/pages/AiAgentsPage.test.tsx �[2m(�[22m�[2m11 tests�[22m�[2m)�[22m�[33m 1956�[2mms�[22m�[39m
   �[32m✓�[39m AiAgentsPage model selection �[2m(11)�[22m
     �[33m�[2m✓�[22m�[39m places each desktop header and content region in the same resizable pane�[33m 390�[2mms�[22m�[39m
     �[33m�[2m✓�[22m�[39m replaces Playground selections with the exact enabled agent/model pair and opens the mobile Playground�[33m 445�[2mms�[22m�[39m
 �[32m✓�[39m src/components/Agents/AgentRunDetail.test.tsx �[2m(�[22m�[2m12 tests�[22m�[2m)�[22m�[33m 667�[2mms�[22m�[39m
 �[32m✓�[39m src/components/VoiceBriefingControl.test.tsx �[2m(�[22m�[2m14 tests�[22m�[2m)�[22m�[33m 669�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskDetails/RunTimeline.test.tsx �[2m(�[22m�[2m13 tests�[22m�[2m)�[22m�[33m 959�[2mms�[22m�[39m
   �[32m✓�[39m RunTimeline �[2m(12)�[22m
     �[33m�[2m✓�[22m�[39m keeps all 50 runs in the shared task scroll flow�[33m 398�[2mms�[22m�[39m
�[90mstderr�[2m | src/components/TaskPlanner/useAutoDraftCreation.test.tsx�[2m > �[22m�[2museAutoDraftCreation�[2m > �[22m�[2mkeeps navigating when persisting the resolved baseBranch fails after auto-creating a draft
�[22m�[39mFailed to persist draft setup snapshot: Error: Transient update failure
    at �[90m/home/runner/work/propr/propr/propr-ui/�[39msrc/components/TaskPlanner/useAutoDraftCreation.test.tsx:103:39
    at file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:1628:35
    at file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:2783:26
    at file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:3319:20
    at new Promise (<anonymous>)
    at runWithCancel (file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:3314:10)
    at file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:3299:20
    at new Promise (<anonymous>)
    at runWithTimeout (file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:3257:10)
    at file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:3876:64

�[90mstderr�[2m | src/components/TaskPlanner/useAutoDraftCreation.test.tsx�[2m > �[22m�[2museAutoDraftCreation�[2m > �[22m�[2msurfaces the persistence warning only for in-place auto-created drafts
�[22m�[39mFailed to persist draft setup snapshot: Error: Transient update failure
    at �[90m/home/runner/work/propr/propr/propr-ui/�[39msrc/components/TaskPlanner/useAutoDraftCreation.test.tsx:268:39
    at file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:1628:35
    at file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:2783:26
    at file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:3319:20
    at new Promise (<anonymous>)
    at runWithCancel (file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:3314:10)
    at file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:3299:20
    at new Promise (<anonymous>)
    at runWithTimeout (file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:3257:10)
    at file:///home/runner/work/propr/propr/node_modules/�[4mvitest�[24m/dist/chunks/run.CQOUYP-x.js:3876:64

 �[32m✓�[39m src/components/TaskPlanner/useAutoDraftCreation.test.tsx �[2m(�[22m�[2m12 tests�[22m�[2m)�[22m�[32m 48�[2mms�[22m�[39m
 �[32m✓�[39m src/desktop/nativeCommands.integration.test.tsx �[2m(�[22m�[2m14 tests�[22m�[2m)�[22m�[32m 225�[2mms�[22m�[39m
 �[32m✓�[39m src/components/AssigneeList.test.tsx �[2m(�[22m�[2m25 tests�[22m�[2m)�[22m�[33m 391�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskDetails/useTaskAssignment.test.tsx �[2m(�[22m�[2m11 tests�[22m�[2m)�[22m�[33m 765�[2mms�[22m�[39m
�[90mstderr�[2m | src/components/QuickAddTodo.test.tsx�[2m > �[22m�[2mEscape in the toolbar popover�[2m > �[22m�[2mdismisses the popover opened by the native New To-do shortcut
�[22m�[39mAn update to QuickAddTodo inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to QuickAddTodo inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/components/QuickAddTodo.test.tsx�[2m > �[22m�[2mEscape in the toolbar popover�[2m > �[22m�[2mdismisses with nothing focused, but ignores Escape from controls outside it
�[22m�[39mAn update to QuickAddTodo inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to QuickAddTodo inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/pages/SettingsPage/AgentConfigModal.test.tsx�[2m > �[22m�[2mAgentConfigModal�[2m > �[22m�[2mpreserves distinct migrated effort labels when editing and saving an Antigravity agent
�[22m�[39mAn update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/pages/SettingsPage/AgentConfigModal.test.tsx�[2m > �[22m�[2mAgentConfigModal�[2m > �[22m�[2madds a loginable agent with an isolated managed credential path and requests login
�[22m�[39mAn update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

 �[32m✓�[39m src/components/QuickAddTodo.test.tsx �[2m(�[22m�[2m17 tests�[22m�[2m)�[22m�[33m 3141�[2mms�[22m�[39m
   �[32m✓�[39m QuickAddTodo popover �[2m(2)�[22m
     �[33m�[2m✓�[22m�[39m keeps the success confirmation and Add another available until dismissed�[33m 405�[2mms�[22m�[39m
�[90mstderr�[2m | src/pages/SettingsPage/AgentConfigModal.test.tsx�[2m > �[22m�[2mAgentConfigModal�[2m > �[22m�[2mallows a new agent to reuse an existing config directory instead of logging in
�[22m�[39mAn update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/pages/SettingsPage/AgentConfigModal.test.tsx�[2m > �[22m�[2mAgentConfigModal�[2m > �[22m�[2muses the agent alias in long model labels
�[22m�[39mAn update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/pages/SettingsPage/AgentConfigModal.test.tsx�[2m > �[22m�[2mAgentConfigModal�[2m > �[22m�[2msaves a model-specific reasoning level for Codex agents
�[22m�[39mAn update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/pages/SettingsPage/AgentConfigModal.test.tsx�[2m > �[22m�[2mAgentConfigModal�[2m > �[22m�[2mmarks a legacy cross-agent reasoning value as unsupported
�[22m�[39mAn update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/pages/SettingsPage/AgentConfigModal.test.tsx�[2m > �[22m�[2moffers one Antigravity entry per model with model-specific reasoning choices
�[22m�[39mAn update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
An update to AgentConfigModal inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

 �[32m✓�[39m src/pages/SettingsPage/AgentConfigModal.test.tsx �[2m(�[22m�[2m9 tests�[22m�[2m)�[22m�[33m 1121�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskDetails/ContextStrip.test.tsx �[2m(�[22m�[2m17 tests�[22m�[2m)�[22m�[33m 438�[2mms�[22m�[39m
�[90mstderr�[2m | src/components/TaskDetails/ReviewScoreHistory.test.tsx�[2m > �[22m�[2musePullRequestScores�[2m > �[22m�[2mnever returns a previous PR's history for a new PR, while loading or after a failed read
�[22m�[39mAn update to TestComponent inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

 �[32m✓�[39m src/components/TaskDetails/ReviewScoreHistory.test.tsx �[2m(�[22m�[2m10 tests�[22m�[2m)�[22m�[32m 292�[2mms�[22m�[39m
�[90mstderr�[2m | src/components/TaskDetails/LatestRunCommands.projection.test.tsx�[2m > �[22m�[2mfollow-up commands read from the API task projection�[2m > �[22m�[2mthe task header keeps /fix for findings and sends it only after the projection is read again
�[22m�[39mAn update to FollowupCommands inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

 �[32m✓�[39m src/components/TaskDetails/LatestRunCommands.projection.test.tsx �[2m(�[22m�[2m13 tests�[22m�[2m)�[22m�[32m 261�[2mms�[22m�[39m
 �[32m✓�[39m src/pages/PlanStudioPage.inPlaceDraft.test.tsx �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 199�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskDetails/index.followupTarget.test.tsx �[2m(�[22m�[2m7 tests�[22m�[2m)�[22m�[33m 503�[2mms�[22m�[39m
 �[32m✓�[39m src/components/ApplicationShell.idle.test.tsx �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 67�[2mms�[22m�[39m
 �[32m✓�[39m src/components/ui/Skeleton.test.tsx �[2m(�[22m�[2m12 tests�[22m�[2m)�[22m�[32m 230�[2mms�[22m�[39m
 �[32m✓�[39m src/desktop-deep-link.test.ts �[2m(�[22m�[2m9 tests�[22m�[2m)�[22m�[32m 10�[2mms�[22m�[39m
 �[32m✓�[39m src/pages/SettingsPage/AgentRuntimePackagesSection.test.tsx �[2m(�[22m�[2m7 tests�[22m�[2m)�[22m�[33m 665�[2mms�[22m�[39m
   �[32m✓�[39m AgentRuntimePackagesSection �[2m(7)�[22m
     �[33m�[2m✓�[22m�[39m offers catalog suggestions and validates a selected package�[33m 336�[2mms�[22m�[39m
 �[32m✓�[39m src/components/Goals/GoalAttentionPanel.test.tsx �[2m(�[22m�[2m5 tests�[22m�[2m)�[22m�[33m 317�[2mms�[22m�[39m
 �[32m✓�[39m src/pages/SettingsPage/NotificationSettingsSection.test.tsx �[2m(�[22m�[2m7 tests�[22m�[2m)�[22m�[33m 931�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskDetails/LogFilesModal.test.tsx �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[33m 483�[2mms�[22m�[39m
   �[32m✓�[39m LogFilesModal �[2m(3)�[22m
     �[33m�[2m✓�[22m�[39m searches literal metacharacters, counts what it highlights, and steps through matches�[33m 371�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskDetails/FollowupModal.test.tsx �[2m(�[22m�[2m5 tests�[22m�[2m)�[22m�[33m 330�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskDetails/TaskVisualPreviews.test.tsx �[2m(�[22m�[2m5 tests�[22m�[2m)�[22m�[33m 380�[2mms�[22m�[39m
 �[32m✓�[39m src/pages/SettingsPage/AgentNetworkSettingsSection.test.tsx �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[33m 348�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/planDisplayName.test.ts �[2m(�[22m�[2m17 tests�[22m�[2m)�[22m�[32m 9�[2mms�[22m�[39m
 �[32m✓�[39m src/api/currentUserResponse.test.ts �[2m(�[22m�[2m5 tests�[22m�[2m)�[22m�[32m 12�[2mms�[22m�[39m
�[90mstdout�[2m | src/components/TaskDetails/LiveFileChips.test.tsx�[2m > �[22m�[2mLiveFileChips live refreshes�[2m > �[22m�[2mturns the fixture baseline of three burst invalidations into one additional file-changes request
�[22m�[39m[LiveFileChips] Received task update, refreshing file changes: { taskId: �[32m'task-1'�[39m }
[LiveFileChips] Received task update, refreshing file changes: { taskId: �[32m'task-1'�[39m }
[LiveFileChips] Received task update, refreshing file changes: { taskId: �[32m'task-1'�[39m }

 �[32m✓�[39m src/components/TaskDetails/LiveFileChips.test.tsx �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[33m 349�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/PlanIssuesManager.notificationIntent.test.tsx �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[33m 607�[2mms�[22m�[39m
   �[32m✓�[39m PlanIssuesManager execution intent �[2m(3)�[22m
     �[33m�[2m✓�[22m�[39m cancel closes without starting implementation�[33m 345�[2mms�[22m�[39m
 �[32m✓�[39m src/components/globalSearchModel.test.ts �[2m(�[22m�[2m7 tests�[22m�[2m)�[22m�[32m 7�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/PlanEditor.notificationIntent.test.tsx �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[33m 539�[2mms�[22m�[39m
   �[32m✓�[39m PlanEditor notification intents �[2m(4)�[22m
     �[33m�[2m✓�[22m�[39m never approves from navigation and cancel leaves the plan unchanged�[33m 494�[2mms�[22m�[39m
 �[32m✓�[39m src/hooks/useCurrentUserBootstrap.mainProxy.test.tsx �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 78�[2mms�[22m�[39m
 �[32m✓�[39m src/components/RepositoryAutoAssignControl.test.tsx �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[32m 258�[2mms�[22m�[39m
 �[32m✓�[39m src/desktop/packagedAcceptanceRendererLifecycle.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 9�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/TaskCard.test.tsx �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[33m 490�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/planIssueDefaultSelection.test.ts �[2m(�[22m�[2m5 tests�[22m�[2m)�[22m�[32m 6�[2mms�[22m�[39m
 �[32m✓�[39m src/pages/SettingsPage/AgentWatchdogSettingsSection.test.tsx �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 77�[2mms�[22m�[39m
�[90mstdout�[2m | src/components/TaskDetails/useTaskData.completion.test.ts�[2m > �[22m�[2mreplaces the live view of a subscribed client with persisted details when the task completes
�[22m�[39m[useTaskData] Received task update via WebSocket: { taskId: �[32m'task'�[39m, state: �[32m'COMPLETED'�[39m }

 �[32m✓�[39m src/components/TaskDetails/useTaskData.completion.test.ts �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 29�[2mms�[22m�[39m
 �[32m✓�[39m src/api/dashboardApi.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 14�[2mms�[22m�[39m
 �[32m✓�[39m src/pages/SettingsPage/VoiceSettingsSection.test.tsx �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[33m 302�[2mms�[22m�[39m
 �[32m✓�[39m src/desktop/DesktopExperience.accounts.test.tsx �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[33m 363�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/generationTelemetryUtils.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 5�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskDetails/PromptModal.test.tsx �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 204�[2mms�[22m�[39m
 �[32m✓�[39m src/components/UserAvatar.test.tsx �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 178�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/setupWizardHooks.editMode.test.tsx �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 24�[2mms�[22m�[39m
�[90mstderr�[2m | src/components/RepositoryAutoResolveConflictsControl.test.tsx�[2m > �[22m�[2mRepositoryAutoResolveConflictsControl�[2m > �[22m�[2mreflects a stored override
�[22m�[39mAn update to RepositoryAutoResolveConflictsControl inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

�[90mstderr�[2m | src/components/RepositoryAutoResolveConflictsControl.test.tsx�[2m > �[22m�[2mRepositoryAutoResolveConflictsControl�[2m > �[22m�[2msets always, never, and inherit (null)
�[22m�[39mAn update to RepositoryAutoResolveConflictsControl inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

 �[32m✓�[39m src/components/RepositoryAutoResolveConflictsControl.test.tsx �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 180�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/ContextLevelSlider.test.tsx �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 51�[2mms�[22m�[39m
 �[32m✓�[39m src/components/modalShells.test.tsx �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 171�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskList/durations.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 5�[2mms�[22m�[39m
 �[32m✓�[39m src/components/RouteChunkErrorBoundary.test.tsx �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 155�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/PlanEditorDesktopLayout.assistant.test.tsx �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 255�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/useSelectedModelMaxTokens.test.ts �[2m(�[22m�[2m5 tests�[22m�[2m)�[22m�[32m 72�[2mms�[22m�[39m
 �[32m✓�[39m src/desktop/scopedTaskEvents.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 6�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/setupWizardDraftConfig.test.ts �[2m(�[22m�[2m9 tests�[22m�[2m)�[22m�[32m 6�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskList/Pagination.test.tsx �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[32m 192�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/TaskCardList.breakpoint.test.tsx �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 193�[2mms�[22m�[39m
 �[32m✓�[39m src/hooks/repositoryNonBlockingChecks.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 5�[2mms�[22m�[39m
 �[32m✓�[39m src/api/automationRunTrigger.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 9�[2mms�[22m�[39m
 �[32m✓�[39m src/components/Agents/AgentAutonomySection.test.tsx �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 181�[2mms�[22m�[39m
 �[32m✓�[39m src/pages/InboxPageComponents.test.tsx �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 180�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskDetails/syntaxHighlighter.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 9�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskDetails/taskKind.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 5�[2mms�[22m�[39m
 �[32m✓�[39m src/api/proprApi.instanceCatalog.test.ts �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 9�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskDetails/AssignmentControl.test.tsx �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 170�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/executionConfigSummary.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 5�[2mms�[22m�[39m
 �[32m✓�[39m src/components/Dashboard/KPICard.test.tsx �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 31�[2mms�[22m�[39m
 �[32m✓�[39m src/components/Repositories/RepoBrowsePanel.test.tsx �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 22�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/RefinementChat.focus.test.tsx �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 153�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskDetails/ExecutionEventUtils.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 5�[2mms�[22m�[39m
 �[32m✓�[39m src/components/TaskPlanner/tokenFormat.test.ts �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 4�[2mms�[22m�[39m

�[2m Test Files �[22m �[1m�[32m82 passed�[39m�[22m�[90m (82)�[39m
�[2m      Tests �[22m �[1m�[32m655 passed�[39m�[22m�[90m (655)�[39m
�[2m   Start at �[22m 00:43:00
�[2m   Duration �[22m 63.04s�[2m (environment 43%, tests 33%, import 10%, setup 8%, transform 5%)�[22m

�[2mEnvironment �[22m �[33mjsdom was created 82 times�[39m�[2m · 50.97s total, 43% of tracked time�[22m
�[2m            �[22m �[2mcreate it once per worker with �[22m�[33mpool: 'vmThreads'�[39m�[2m (keeps per-file isolation) or �[22m�[33misolate: false�[39m�[2m (shares it across files)�[22m
�[2m            �[22m �[2mlearn more: https://vitest.dev/guide/improving-performance#test-environments�[22m


> propr-ui@0.0.1 posttest
> npm run test:docker-context


> propr-ui@0.0.1 test:docker-context
> node --test scripts/docker-context-inputs.test.mjs

TAP version 13
# Subtest: focused UI selectors are forwarded only to Vitest
ok 1 - focused UI selectors are forwarded only to Vitest
  ---
  duration_ms: 0.711838
  type: 'test'
  ...
# Subtest: the UI Docker context contains its complete non-type external source import closure
ok 2 - the UI Docker context contains its complete non-type external source import closure
  ---
  duration_ms: 52.37452
  type: 'test'
  ...
1..2
# tests 2
# suites 0
# pass 2
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 98.266164
[230/230] passed propr-ui#1/4 (workspace test script) in 63.7s

### Shard 1/4: 229/230 passed in 870.6s

Assigned 230 of 920 discovered units.

| Duration | Status | Unit |
| ---: | --- | --- |
| 158.6s | passed | `test/taskIntentWithdrawal.test.ts` |
| 63.7s | passed | `propr-ui#1/4` (workspace) |
| 63.5s | passed | `packages/api/test/mcpAccessLog.test.ts` |
| 63.1s | passed | `packages/api/test/mcpAgentRuns.test.ts` |
| 39.6s | passed | `packages/api/test/taskFollowupIssueQueueRestarts.test.ts` |
| 31.4s | passed | `packages/core/test/fleetDataPlane.test.ts` |
| 28.0s | passed | `packages/api/test/agentDefinitionRoutes.test.ts` |
| 21.2s | passed | `test/prPublicationRecoveryJob.test.ts` |
| 16.9s | passed | `test/agentRunStore.test.ts` |
| 15.9s | passed | `apps/desktop/scripts/electron-pairing-zstd.test.mjs` |
| 15.8s | passed | `packages/core/test/managedDiagnosticsBatching.test.ts` |
| 11.5s | passed | `packages/api/test/mcpWorkflows.test.ts` |
| 11.1s | passed | `packages/api/test/taskAssignmentRoutes.test.ts` |
| 9.8s | passed | `test/integrationImageHarnessNetworkOwnership.test.mjs` |
| 9.3s | passed | `packages/core/test/eventPublisherBounds.test.ts` |

Passing units at or above 60% of the 180.0s per-unit timeout. Split or speed these up before they fail:

- `test/taskIntentWithdrawal.test.ts` used 88% (158.6s)

::warning title=Test unit near the per-unit timeout::test/taskIntentWithdrawal.test.ts used 88% (158.6s) of the 180.0s per-unit timeout

1/230 test runs failed in shard 1/4 after 870.6s:
- test/ultrafixRecoveryRedisIntegration.test.ts: exit 1

Details truncated; see the uploaded artifacts for complete failure rows and logs.

View Workflow
View uploaded artifacts

@integry integry added the AI label Oct 20, 2026
…e mock

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@integry

integry commented Oct 20, 2026

Copy link
Copy Markdown
Owner Author

/review

@propr-dev

propr-dev Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

✅ AI Code Review Complete requested by @integry

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

🔍 AI Code Review — codex:gpt-6-astra

Overall Evaluation

This PR addresses stalled auto-merges by adding an Ultrafix fallback and periodic reevaluation. It needs changes before merge: the new no-CI path can mistake incomplete API results for absent CI, and its grace period does not reliably cover newly pushed heads.

✅ Shared merge gates — Both new entry points reuse label, Ultrafix, mergeability, active-work, and repository-policy checks.

✅ Sweep failure isolation — A failed candidate does not prevent subsequent PRs from being evaluated, and overlapping ticks are suppressed.

✅ Current checks pass — The supplied authoritative status reports 20 passed checks and no failed or pending checks; the historical shard failure is not current.

Merge blockers

Every finding below was introduced by this PR and must be resolved before merging.

F1: 🔴 Preserve CI fetch failures before declaring no CI

  • Required behavior: API errors must block the new no-CI merge path; unavailable CI information must not count as evidence that CI is absent.

  • Evidence: packages/core/src/webhook/checkRunHelpers.ts:getCiMergeReadiness

    1. An eligible PR has an older head, passes the other merge gates, and has no required branch-protection checks.
    2. Its check-runs request fails, while its legacy status request succeeds with zero statuses.
    3. fetchCheckSignals suppresses the individual failure and returns empty check runs plus zero statuses.
    4. getCiMergeReadiness fetches the commit date and returns { ready: true, reason: 'no_ci' }.
    5. The new fallback or sweep can merge without establishing whether blocking check runs exist.

    The symmetric case occurs when the status request fails and check runs are empty. The outer catch cannot help because the shared fetcher throws only when both requests fail. A clean mergeability result does not establish that an unreadable, non-required CI signal is absent. The added API-failure test rejects both requests, so it misses this sequence.

    static trace: verified against the supplied fetcher, readiness helper, and merge-handler control flow. Before this change, an empty result failed areAllChecksPassing through its hasCheckSignal guard.

  • Minimum fix: Preserve whether each CI request succeeded and require both successful responses before returning no_ci. Add regression cases for each individual request failing while the other returns empty.

F2: 🔴 Measure CI grace from head observation

  • Required behavior: The grace period must give checks time to register before a newly introduced PR head is treated as having no CI.

  • Evidence: packages/core/src/webhook/checkRunHelpers.ts:getCiMergeReadiness, specifically the comparison against commit.data.commit.committer?.date.

    1. A developer creates a commit locally and pushes it more than two minutes later to an eligible, auto-merge-labelled plan PR.
    2. The daemon sweep evaluates that head before its CI checks have registered. Both CI endpoints return empty results.
    3. The helper compares the current time with the commit's embedded committer date, so the grace period is already considered elapsed on the first observation.
    4. If GitHub reports the PR clean and the remaining gates allow merging, the sweep merges it before CI begins.

    A commit timestamp records neither its push time nor when it became the PR head. The head-SHA checks do not prevent this sequence because the head stays unchanged throughout. The clean-state gate protects required checks, but does not supply the promised registration delay for CI that is not required by branch protection.

    static trace: verified from the supplied timestamp comparison and sweep-to-merge path. The added “fresh head” test uses a fresh commit timestamp, so it does not cover an older commit newly pushed to a PR.

  • Minimum fix: Start the no-signal grace period from a trustworthy observation of the PR/head SHA, preserving that observation across sweep ticks and resetting it for a different head. Add a regression showing that an old commit newly observed as the head still waits.

Suggestions

These are optional follow-ups and are not sent to /fix.

S1: 🟢 Exercise sweep candidate selection

The sweep test injects candidates, leaving the actual database query untested. A focused test covering duplicate PR links, terminal statuses, and the 14-day boundary would improve confidence in candidate selection. This is optional coverage; no query defect is established by the supplied code.

Score

The shared-gate approach and passing current checks are strong, but two defects in the new no-CI decision can permit premature merges. Both can be corrected locally.

Score: 6/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 48s
  • Tokens: 59,815 (58,628 in / 1,187 out)
  • Cost: $0.50

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix F1 F2 S1

🤖 Review by ProPR

@integry

integry commented Oct 20, 2026

Copy link
Copy Markdown
Owner Author

/ultrafix

@propr-dev propr-dev Bot added the ultrafix label Oct 20, 2026
@propr-dev

propr-dev Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

🔄 Ultrafix loop started (goal: 8/10, max cycles: 10)

First action: /fix

💡 Tip: Remove the ultrafix label from this PR to stop further ultrafix cycles.

@propr-dev

propr-dev Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

✅ Applied the requested follow-up changes in commit e4b7a8c

Addressed 1 AI review comment (IDs: 6287348713)

Addressed findings F1, F2

Summary of Changes

I've fixed F1 and F2. Both are in getCiMergeReadiness (packages/core/src/webhook/checkRunHelpers.ts), plus its one caller in checkRunHandler.ts.

F1: a failed CI request no longer counts as "no CI"

  • The shared CI fetcher now also reports whether both requests (check runs and legacy statuses) succeeded.
  • If no CI shows up and either request failed, the answer is now unavailable, which blocks the merge. It no longer returns no_ci, and the 2-minute wait doesn't start.
  • The other callers (getCheckRunsStatus, areAllChecksPassing) ignore the new flag, so their behaviour is unchanged.

F2: the 2-minute wait starts when ProPR first sees the head

  • The commit-date lookup is gone. A new helper, observePRHead, saves the head SHA and the time ProPR first saw it with no CI. It is stored in Redis per PR (auto-merge:head-observed:<owner>:<repo>:<pr>, expiring after 30 days).
  • A later sweep run, a webhook, or another process reuses the same saved time, so it carries over between sweep runs.
  • A different head replaces the saved entry and restarts the wait. Returning to an earlier head also starts over rather than reusing the old time.
  • If two processes write at once, the saved time can only move later, so the wait never gets shorter. A Redis failure blocks the merge as unavailable.
  • getCiMergeReadiness now needs the PR number: the call is now getCiMergeReadiness(owner, repo, ref, { prNumber }). The Ultrafix fallback and the sweep both go through this one call site.

Tests (test/checkRunHandler.test.ts)

  • One test each for the check-runs request failing and the status request failing while the other comes back empty. Both get unavailable and don't start the wait. With the F1 fix removed, both fail.
  • An old commit seen as the head for the first time still waits, and no commit lookup is made.
  • The saved time carries over between runs: still waiting at 2 minutes minus 1 ms, mergeable at 2 minutes.
  • A different head restarts the wait, and so does returning to an earlier one.
  • A broken Redis blocks the merge.

checkRunHandler, the auto-merge fallback, the sweep and the related Ultrafix suites all pass (221 tests). Core lint with zero warnings, core tsc, and the root tsc all pass.

Side effects to know about:

  • If the GitHub App can't read legacy commit statuses on a repo (the "Resource not accessible by integration" response), a repo with no CI now never merges through this path; it stays unavailable. That follows the rule that unreadable CI isn't evidence of no CI, but it is stricter than before.
  • The first sighting is only recorded once the earlier gates (labels, Ultrafix state, head match) pass. So the wait can start later than the actual push. That only makes it more cautious.

There's nothing visual in this change, so I didn't capture any previews.


🤖 Implementation Details

  • Model: Claude Opus 5.5 (medium)
  • Requested By: @integry
  • Time: 4m 50s
  • Tokens: 695,246 (682,898 in / 12,348 out)
  • Cost: $0.65

Undo Changes • View Task Execution


💡 ProPR Slash Commands
Command Description Example
/merge Merge target branch into this PR and resolve conflicts /merge
/review Request an AI code review /review or /review claude-sonnet
/fix Address review blockers, or blockers and suggestions with /fix all /fix, /fix all, or /fix F3 S5
/switch Change the AI model for this PR /switch claude-opus
/use Override the model for a single follow-up run /use claude-sonnet
/ultrafix Loop review→fix cycles until score goal is met /ultrafix or /ultrafix goal=8 max=10

🤖 Generated by ProPR
Processing comment ID: 6287455285✓

…ackages/core/src/webhook/checkRunHelpers.ts`), plus its one caller in `checkRunHandler.ts`.

I've fixed F1 and F2. Both are in `getCiMergeReadiness` (`packages/core/src/webhook/checkRunHelpers.ts`), plus its one caller in `checkRunHandler.ts`.

**F1: a failed CI request no longer counts as "no CI"**
- The shared CI fetcher now also reports whether both requests (check runs and legacy statuses) succeeded.
- If no CI shows up and either request failed, the answer is now `unavailable`, which blocks the merge. It no longer returns `no_ci`, and the 2-minute wait doesn't start.
- The other callers (`getCheckRunsStatus`, `areAllChecksPassing`) ignore the new flag, so their behaviour is unchanged.

**F2: the 2-minute wait starts when ProPR first sees the head**
- The commit-date lookup is gone. A new helper, `observePRHead`, saves the head SHA and the time ProPR first saw it with no CI. It is stored in Redis per PR (`auto-merge:head-observed:<owner>:<repo>:<pr>`, expiring after 30 days).
- A later sweep run, a webhook, or another process reuses the same saved time, so it carries over between sweep runs.
- A different head replaces the saved entry and restarts the wait. Returning to an earlier head also starts over rather than reusing the old time.
- If two processes write at once, the saved time can only move later, so the wait never gets shorter. A Redis failure blocks the merge as `unavailable`.
- `getCiMergeReadiness` now needs the PR number: the call is now `getCiMergeReadiness(owner, repo, ref, { prNumber })`. The Ultrafix fallback and the sweep both go through this one call site.

**Tests** (`test/checkRunHandler.test.ts`)
- One test each for the check-runs request failing and the status request failing while the other comes back empty. Both get `unavailable` and don't start the wait. With the F1 fix removed, both fail.
- An old commit seen as the head for the first time still waits, and no commit lookup is made.
- The saved time carries over between runs: still waiting at 2 minutes minus 1 ms, mergeable at 2 minutes.
- A different head restarts the wait, and so does returning to an earlier one.
- A broken Redis blocks the merge.

`checkRunHandler`, the auto-merge fallback, the sweep and the related Ultrafix suites all pass (221 tests). Core lint with zero warnings, core `tsc`, and the root `tsc` all pass.

**Side effects to know about:**
- If the GitHub App can't read legacy commit statuses on a repo (the "Resource not accessible by integration" response), a repo with no CI now never merges through this path; it stays `unavailable`. That follows the rule that unreadable CI isn't evidence of no CI, but it is stricter than before.
- The first sighting is only recorded once the earlier gates (labels, Ultrafix state, head match) pass. So the wait can start later than the actual push. That only makes it more cautious.

There's nothing visual in this change, so I didn't capture any previews.

PR: #3280
Comment by: @integry (ID: 6287455285)
Model: Claude Opus 5.5
@propr-dev

propr-dev Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

✅ AI Code Review Complete requested by @propr-ultrafix

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

🔍 AI Code Review — codex:gpt-6-astra

Overall Evaluation

The PR addresses stalled auto-merges with an Ultrafix fallback and periodic reconciliation. It needs localized corrections before merge.

✅ Shared merge gates — Both new entry points reuse the existing label, Ultrafix, mergeability, active-work, and repository-policy checks.

✅ Conservative empty-CI handling — Empty results require both endpoints to succeed, and observation-store failures block merging.

✅ Current checks pass — The supplied authoritative status reports 20 passed, no failures, and no pending checks.

Merge blockers

Every finding below was introduced by this PR and must be resolved before merging.

F3: 🔴 Preserve the resolved merge method

  • Required behavior: The new direct-merge fallback must complete eligible merges using a method supported by the repository.

  • Evidence: src/jobs/ultrafixLoopGithub.ts:maybeEnableAutoMerge, new reevaluatePRAutoMerge call.

    1. A repository permits merge commits but disables squash merging, and its policy does not explicitly specify a method.
    2. After Ultrafix succeeds, gateAutoMergeArming resolves MERGE, as demonstrated by the supplied existing policy test.
    3. GitHub refuses auto-merge because the repository setting is disabled. The new fallback invokes reevaluatePRAutoMerge without the resolved method.
    4. Reevaluation obtains a policy decision without method; performMergeAndPostActions consequently defaults to squash.
    5. The merge request fails. The new daemon sweep repeats the same method selection, leaving this eligible PR stalled.

    The policy and clean-mergeability gates do not resolve repository-supported methods. The fallback tests mock reevaluation, so they do not exercise this mismatch. Verification provenance: static trace through the supplied fallback, handler, policy implementation, and repository-method test.

  • Minimum fix: Resolve a repository-supported method in the shared direct-merge path when policy omits one, using the same resolution as the arming gate. This fixes both fallback and sweep entry points.

F4: 🔴 Reset observations across heads with CI

  • Required behavior: A different observed PR head must invalidate the previous head’s no-CI grace period, including when the intermediate head has CI.

  • Evidence: packages/core/src/webhook/checkRunHelpers.ts:getCiMergeReadiness, signal-present return before observePRHead.

    1. A sweep observes head A with no CI and records its timestamp; merging waits for the grace period.
    2. The PR moves to head B. A subsequent sweep sees B’s pending checks and returns checks_not_passing without updating the stored head observation.
    3. After A’s original timestamp is more than two minutes old, the branch is reset to A.
    4. The next sweep sees no CI for A. observePRHead finds the retained A timestamp and immediately returns no_ci, allowing a merge before the renewed observation grace period has elapsed.

    The current-head comparisons and merge SHA guard accept A because it is again the current head. Neither detects reuse of the stale observation. The added head-switching test uses empty CI responses throughout, so it misses the intermediate-head-with-CI case. Verification provenance: static trace; proposed regression: exercise A without CI → B with pending CI → A without CI and require a fresh grace period.

  • Minimum fix: Invalidate or replace the previous no-CI observation when an evaluated head has CI, ensuring a return to an earlier head starts a fresh no-CI grace period.

Suggestions

These are optional follow-ups and are not sent to /fix.

S2: 🟢 Cover partial failures with signals

The new readiness helper checks complete only after its signal-present branch. Consequently, a successful endpoint can permit readiness while the other endpoint failed. This behavior already existed in areAllChecksPassing, so it is not a PR-introduced blocker. A follow-up should clarify that contract and test partial failures with nonempty results; the new tests currently cover only empty surviving responses.

Score

The implementation is focused and current checks pass, but two reachable gaps remain in the new fallback and grace-period behavior. This review used static tracing only, as requested.
Score: 6/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 1m 5s
  • Tokens: 62,414 (60,939 in / 1,475 out)
  • Cost: $0.66

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix F3 F4 S2

🤖 Review by ProPR

@propr-dev

propr-dev Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

✅ Applied the requested follow-up changes in commit c1da9a4

Addressed 1 AI review comment (IDs: 6287925494)

Addressed findings F3, F4

Summary of Changes

I fixed F3 and F4. The fixes are in the shared direct-merge path, so the check-event merge, the Ultrafix fallback and the daemon sweep all get them.

F3: merge method. Direct merges now use the same repository-supported method that arming uses.

  • packages/core/src/services/autoMergeGate.ts has a new export, resolveDirectMergeMethod. It returns the policy's method if one is set. Otherwise it uses the same lookup as gateAutoMergeArming: squash, unless the repository disallows squash, in which case merge commit, then rebase.
  • processPRAutoMerge in packages/core/src/webhook/checkRunHandler.ts calls it before performMergeAndPostActions, so a policy without a method no longer falls back to squash.
  • processPRAutoMerge is the only place that calls performMergeAndPostActions, so no other merge path has the same bug.

F4: stale no-CI observation. Seeing CI on any head now ends the earlier no-CI grace period.

  • In getCiMergeReadiness (packages/core/src/webhook/checkRunHelpers.ts), when the evaluated head has check runs or statuses, the PR's stored head observation is deleted.
  • So A without CI → B with CI → back to A records A again and waits a fresh 2 minutes.
  • Two concurrent sweeps can only make the observation later, never earlier, so the grace period can't be cut short.
  • One behaviour to know: if Redis is unavailable, that delete fails and the PR is reported unavailable, so passing checks don't merge either. That matches how an unreadable observation store already blocks merging.

New tests

  • In test/checkRunHandler.test.ts:
    • The A (no CI) → B (pending CI) → A (no CI) case.
    • A check-event merge where the policy gives no method: the resolver's merge is what reaches GitHub's merge request.
  • In packages/core/test/autoMergeGate.test.ts: resolveDirectMergeMethod returns merge, rebase or squash depending on repository settings, and a configured method takes precedence.

I reverted each fix temporarily and the matching new test failed, so they catch the original bugs.

Verification

  • checkRunHandler: 120 pass. autoMergeGate: 30 pass.
  • ultrafixAutoMergeFallback, autoMergeSweep, ultrafixCiFailureRecovery, ultrafixContinuationEntryPoint, ultrafixRecoveryIntake, taskIntentWithdrawal and e2e/webhook all pass.
  • tsc on packages/core is clean. ESLint shows no errors or warnings on the changed source files; the test files are outside its config and were skipped, not linted.

I found nothing else to report outside F3 and F4, and the change has no visible UI, so there are no previews.


🤖 Implementation Details

  • Model: Claude Opus 5.5 (medium)
  • Requested By: @propr-ultrafix
  • Time: 8m 11s
  • Tokens: 1,636,218 (1,635,941 in / 277 out)
  • Cost: $0.69
  • Subscription usage: Session +1%, Fable +1%

View Task Execution


💡 ProPR Slash Commands
Command Description Example
/merge Merge target branch into this PR and resolve conflicts /merge
/review Request an AI code review /review or /review claude-sonnet
/fix Address review blockers, or blockers and suggestions with /fix all /fix, /fix all, or /fix F3 S5
/switch Change the AI model for this PR /switch claude-opus
/use Override the model for a single follow-up run /use claude-sonnet
/ultrafix Loop review→fix cycles until score goal is met /ultrafix or /ultrafix goal=8 max=10

🤖 Generated by ProPR

… path, so the check-event merge, the Ultrafix fallback and the daemon sweep all get them.

I fixed F3 and F4. The fixes are in the shared direct-merge path, so the check-event merge, the Ultrafix fallback and the daemon sweep all get them.

**F3: merge method.** Direct merges now use the same repository-supported method that arming uses.
- `packages/core/src/services/autoMergeGate.ts` has a new export, `resolveDirectMergeMethod`. It returns the policy's method if one is set. Otherwise it uses the same lookup as `gateAutoMergeArming`: squash, unless the repository disallows squash, in which case merge commit, then rebase.
- `processPRAutoMerge` in `packages/core/src/webhook/checkRunHandler.ts` calls it before `performMergeAndPostActions`, so a policy without a method no longer falls back to `squash`.
- `processPRAutoMerge` is the only place that calls `performMergeAndPostActions`, so no other merge path has the same bug.

**F4: stale no-CI observation.** Seeing CI on any head now ends the earlier no-CI grace period.
- In `getCiMergeReadiness` (`packages/core/src/webhook/checkRunHelpers.ts`), when the evaluated head has check runs or statuses, the PR's stored head observation is deleted.
- So A without CI → B with CI → back to A records A again and waits a fresh 2 minutes.
- Two concurrent sweeps can only make the observation later, never earlier, so the grace period can't be cut short.
- One behaviour to know: if Redis is unavailable, that delete fails and the PR is reported `unavailable`, so passing checks don't merge either. That matches how an unreadable observation store already blocks merging.

**New tests**
- In `test/checkRunHandler.test.ts`:
  - The A (no CI) → B (pending CI) → A (no CI) case.
  - A check-event merge where the policy gives no method: the resolver's `merge` is what reaches GitHub's merge request.
- In `packages/core/test/autoMergeGate.test.ts`: `resolveDirectMergeMethod` returns merge, rebase or squash depending on repository settings, and a configured method takes precedence.

I reverted each fix temporarily and the matching new test failed, so they catch the original bugs.

**Verification**
- `checkRunHandler`: 120 pass. `autoMergeGate`: 30 pass.
- `ultrafixAutoMergeFallback`, `autoMergeSweep`, `ultrafixCiFailureRecovery`, `ultrafixContinuationEntryPoint`, `ultrafixRecoveryIntake`, `taskIntentWithdrawal` and `e2e/webhook` all pass.
- `tsc` on `packages/core` is clean. ESLint shows no errors or warnings on the changed source files; the test files are outside its config and were skipped, not linted.

I found nothing else to report outside F3 and F4, and the change has no visible UI, so there are no previews.

PR: #3280
Comment by: @propr-ultrafix (ID: 0)
Model: Claude Opus 5.5
@propr-dev

propr-dev Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

✅ AI Code Review Complete requested by @propr-ultrafix

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

🔍 AI Code Review — codex:gpt-6-astra

Overall Evaluation

The PR addresses stalled auto-merges with a direct-merge fallback and periodic reevaluation. It needs a localized correctness fix before merge.

✅ Shared merge gates — The fallback and sweep reuse reevaluatePRAutoMerge, preserving policy, active-work, mergeability, and head-SHA checks.

✅ Observation-based grace period — No-CI readiness uses ProPR’s observation time rather than commit timestamps, with tests for head changes and Redis failures.

The authoritative current checks report 20 passed, none failed or pending.

Merge blockers

Every finding below was introduced by this PR and must be resolved before merging.

F5: 🔴 Block merges on incomplete CI reads

  • Required behavior: The stated CI-readiness requirement says API errors must continue to block merging; an unreadable endpoint can conceal pending or failing checks.

  • Evidence: packages/core/src/webhook/checkRunHelpers.ts, getCiMergeReadiness

    1. An eligible PR reaches the new sweep or Ultrafix fallback with passing check runs.
    2. The check-runs request succeeds, but the legacy-status request fails. fetchCheckSignals reports complete: false.
    3. Because check runs exist, getCiMergeReadiness enters the signal-present branch and returns the summary’s passing result before checking complete.
    4. If the remaining merge gates pass, ProPR can merge without establishing whether the inaccessible endpoint contains a blocking status.

    static trace: The if (!complete) guard appears only after the signal-present branch’s unconditional return. The same ordering affects either endpoint failing while the other supplies passing signals. The clean-mergeability gate does not establish that all ProPR-blocking checks were successfully read; checks need not be required by GitHub branch protection. Existing partial-failure tests cover only an empty surviving endpoint.

    proposed regression: Return passing checks while the status request rejects, and passing statuses while the check-runs request rejects; both readiness results must be unavailable.

  • Minimum fix: Check complete immediately after fetching signals, before either passing-CI or no-CI evaluation, and cover both partial-failure cases.

Suggestions

These are optional follow-ups and are not sent to /fix.

S3: 🟢 Test sweep candidate selection

The sweep test injects candidates, so it does not exercise the database query’s status exclusions, deduplication, or fourteen-day boundary. A focused query test would improve confidence in those selection rules. This is optional coverage; no defect in that query is demonstrated.

Score

The implementation is focused and current checks pass, but incomplete CI reads can incorrectly permit the new merge paths. The required correction is localized.

Score: 6/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 46s
  • Tokens: 58,746 (57,773 in / 973 out)
  • Cost: $0.61

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix F5 S3

🤖 Review by ProPR

@propr-dev

propr-dev Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

✅ Applied the requested follow-up changes in commit e01f770

Addressed 1 AI review comment (IDs: 6288457905)

Addressed finding F5

Summary of Changes

I fixed F5: a merge is now blocked whenever either CI endpoint fails to read, even if the other one shows passing checks.

What was wrong: in getCiMergeReadiness (packages/core/src/webhook/checkRunHelpers.ts:527), the check for an incomplete read only ran after the "signals present" branch had already returned. So passing check runs with a failed status request (or the reverse) came back as checks_passing.

The fix: if (!complete) return { ready: false, reason: 'unavailable' } now runs straight after the fetch, before both the passing-CI and no-CI checks. I removed the old copy further down. This function is the only CI gate on the direct-merge path, so the check-event merge, the Ultrafix fallback and the daemon sweep all pick up the fix.

Tests: I added two regressions to the getCiMergeReadiness block in test/checkRunHandler.test.ts:

  • passing check runs while the status request fails → unavailable
  • passing statuses while the check-runs request fails → unavailable

All 122 tests in test/checkRunHandler.test.ts pass. With the old source put back, the two new tests fail and nothing else does. The core package's ESLint (zero warnings allowed) and tsc --noEmit are clean. To run the suite I had to install dependencies with npm ci and build packages/shared; neither changes any repo files.

Separate finding, not changed: two older functions, areAllChecksPassing and getCheckRunsStatus in the same file, also ignore complete. A failed endpoint there can still produce a passing result for the Ultrafix loop and the polling readiness checks. They behaved the same way before this PR and are outside its merge path, so I left them alone. They would need their own finding if you want them fixed.

This change has nothing visible, so I made no preview.


🤖 Implementation Details

  • Model: Claude Opus 5.5 (medium)
  • Requested By: @propr-ultrafix
  • Time: 2m 53s
  • Tokens: 615,563 (611,422 in / 4,141 out)
  • Cost: $0.41

View Task Execution


💡 ProPR Slash Commands
Command Description Example
/merge Merge target branch into this PR and resolve conflicts /merge
/review Request an AI code review /review or /review claude-sonnet
/fix Address review blockers, or blockers and suggestions with /fix all /fix, /fix all, or /fix F3 S5
/switch Change the AI model for this PR /switch claude-opus
/use Override the model for a single follow-up run /use claude-sonnet
/ultrafix Loop review→fix cycles until score goal is met /ultrafix or /ultrafix goal=8 max=10

🤖 Generated by ProPR

…int fails to read, even if the other one shows passing checks.

I fixed F5: a merge is now blocked whenever either CI endpoint fails to read, even if the other one shows passing checks.

**What was wrong:** in `getCiMergeReadiness` (`packages/core/src/webhook/checkRunHelpers.ts:527`), the check for an incomplete read only ran after the "signals present" branch had already returned. So passing check runs with a failed status request (or the reverse) came back as `checks_passing`.

**The fix:** `if (!complete) return { ready: false, reason: 'unavailable' }` now runs straight after the fetch, before both the passing-CI and no-CI checks. I removed the old copy further down. This function is the only CI gate on the direct-merge path, so the check-event merge, the Ultrafix fallback and the daemon sweep all pick up the fix.

**Tests:** I added two regressions to the `getCiMergeReadiness` block in `test/checkRunHandler.test.ts`:
- passing check runs while the status request fails → `unavailable`
- passing statuses while the check-runs request fails → `unavailable`

All 122 tests in `test/checkRunHandler.test.ts` pass. With the old source put back, the two new tests fail and nothing else does. The core package's ESLint (zero warnings allowed) and `tsc --noEmit` are clean. To run the suite I had to install dependencies with `npm ci` and build `packages/shared`; neither changes any repo files.

**Separate finding, not changed:** two older functions, `areAllChecksPassing` and `getCheckRunsStatus` in the same file, also ignore `complete`. A failed endpoint there can still produce a passing result for the Ultrafix loop and the polling readiness checks. They behaved the same way before this PR and are outside its merge path, so I left them alone. They would need their own finding if you want them fixed.

This change has nothing visible, so I made no preview.

PR: #3280
Comment by: @propr-ultrafix (ID: 0)
Model: Claude Opus 5.5
@propr-dev

propr-dev Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

✅ AI Code Review Complete requested by @propr-ultrafix

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

🔍 AI Code Review — codex:gpt-6-astra

Overall Evaluation

The PR addresses stalled auto-merges by adding a gated direct-merge fallback and periodic reevaluation. The supplied code shows no verified merge blocker. It is merge-ready within scope, conditional on the four pending test shards completing successfully.

✅ Shared merge gates — Both the Ultrafix fallback and daemon sweep use reevaluatePRAutoMerge, retaining label, Ultrafix, active-work, mergeability, policy, and head-SHA checks.

✅ Conservative CI readiness — Either CI endpoint failing blocks merging. Heads without CI must complete an observation-based grace period, with tests covering head changes and unavailable storage.

✅ Consistent merge method — Direct merges now resolve the repository-supported default using the same logic as auto-merge arming.

This assessment is a static review of the supplied diff and context; no commands or tests were run.

Merge blockers

No merge blockers.

Suggestions

These are optional follow-ups and are not sent to /fix.

S4: 🟢 Exercise sweep selection and scheduling

test/autoMergeSweep.test.ts verifies candidate dispatch and per-PR failure isolation, but injects the candidates. Additional tests for loadAutoMergeSweepCandidates would verify terminal-status exclusion, duplicate elimination, and the 14-day boundary. Testing overlapping ticks would also protect the scheduler’s concurrency guard. These are optional coverage improvements; no defect in those implementations was established.

S5: 🟢 Test active-work exclusion end to end

The fallback test verifies that the finishing job ID reaches reevaluatePRAutoMerge, but does not exercise the actual filtering in hasActiveTasksForPR. A focused integration test could demonstrate that the finishing work is excluded while another active task or queued job still blocks merging. This would strengthen coverage of a consequential boundary without changing the intended behavior.

Score

The implementation addresses the stated failure paths with conservative gates and targeted regression coverage. The supplied current-head status has no failures, with four test shards still pending.

Score: 8/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 27s
  • Tokens: 57,534 (57,016 in / 518 out)
  • Cost: $0.58

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix S4 S5

🤖 Review by ProPR

@propr-dev propr-dev Bot removed the ultrafix label Oct 20, 2026
@integry
integry merged commit 2d58cf3 into main Oct 20, 2026
45 checks passed
@integry
integry deleted the fix/auto-merge-without-github-automerge branch October 20, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants