Skip to content

Add visual previews to generated pull requests - #2102

Merged
integry merged 18 commits into
mainfrom
visual-previews-b1b77b35
Sep 4, 2026
Merged

Add visual previews to generated pull requests#2102
integry merged 18 commits into
mainfrom
visual-previews-b1b77b35

Conversation

@propr-dev

@propr-dev propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add repository-level visual-preview policies, capture manifests, and GitHub attachment publication for initial implementations and preview-only follow-ups.
  • Install the pinned GitHub CLI in worker images and use a compatible user credential for media while retaining GitHub App authentication for commits, pull requests, and bot-owned comments.
  • Encrypt the upload credential in shared SQLite and coordinate proactive refresh for OAuth grants across processes.
  • Add Settings → Visual preview uploads, where an administrator can paste and validate a PAT without a CLI, service restart, or OAuth callback URL.
  • Let propr setup import a compatible gh auth token automatically when no working preview credential exists; imported static tokens remain isolated from browser sessions.
  • Detect missing, unsupported, expired, and GitHub-rejected upload credentials; show actionable PAT replacement instructions in the affected pull request instead of a generic upload failure.
  • Build the configured unified agent image once on startup/configuration changes and make preview-only requests use the same publication path.

Validation

  • Full server unit suite: 288 passed.
  • Full UI suite: 496 passed.
  • Focused preview credential, attachment publication, API authorization, CLI setup, GitHub session refresh, and Settings UI tests passed.
  • Root, core, API, CLI, and UI typechecks/builds passed.
  • Root, core, API, CLI, and UI lint passed.

The pull request remains a draft for review.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Checks Failed

Validation failed during setup, tests, CLI packaging, linting, or build checks.

View Logs
Build check diagnostics for run 33741029690, attempt 1
Checkout outcome: success

--- GitHub Actions workflow lint ---
docker.io/rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667: Pulling from rhysd/actionlint
589002ba0eae: Pulling fs layer
c09af8888d6a: Pulling fs layer
4ccd7d76ca98: Pulling fs layer
b6b4b7b0e257: Pulling fs layer
b6b4b7b0e257: Waiting
c09af8888d6a: Verifying Checksum
c09af8888d6a: Download complete
589002ba0eae: Verifying Checksum
589002ba0eae: Download complete
4ccd7d76ca98: Verifying Checksum
4ccd7d76ca98: Download complete
589002ba0eae: Pull complete
b6b4b7b0e257: Verifying Checksum
b6b4b7b0e257: Download complete
c09af8888d6a: Pull complete
4ccd7d76ca98: Pull complete
b6b4b7b0e257: Pull complete
Digest: sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667
Status: Downloaded newer image for rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667
docker.io/rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667

--- Release shell script lint ---

--- Toolchain ---
v22.23.2
10.9.8

--- Dependency installation ---

added 979 packages, and audited 985 packages in 20s

297 packages are looking for funding
  run `npm fund` for details

3 vulnerabilities (2 moderate, 1 high)

To address all issues, run:
  npm audit fix

Run `npm audit` for details.

--- Fast unit tests ---
Running fast unit tests (no Redis/Docker dependencies)...

> propr@0.8.15 test:unit
> NODE_ENV=test npx tsx --experimental-test-module-mocks --test test/minimal.test.ts test/modelName.test.ts test/agentContainerResources.test.ts test/agentDockerfileSupplyChain.test.ts test/daemonEventIntake.test.ts test/databaseMigrationGate.test.ts test/deployPrPreview.test.mjs test/generateContext.test.ts test/githubEventIntakeMode.test.ts test/intakeModePrerequisites.test.ts test/orchestratorMigrationPhase.test.mjs test/validateRoutingUrl.test.ts test/routingWebSocketProtocol.test.ts test/routingWebSocketIntakeService.test.ts test/routingStatusPublisher.test.ts test/releaseValidation.test.mjs test/sessionSecret.test.ts test/testSuiteRunner.test.mjs packages/api/test/connectAuth.test.ts packages/api/test/attachmentUploadCleanup.test.ts packages/api/test/configReloadSubscription.test.ts packages/api/test/dockerCommandSafety.test.ts packages/api/test/listenAddress.test.ts packages/api/test/oauthState.test.ts packages/api/test/requestRateLimits.test.ts packages/api/test/statusRoutes.test.ts packages/api/test/agentRuntimeRoutes.test.ts packages/api/test/instanceAuthorization.test.ts packages/api/test/routeAuthorization.test.ts

TAP version 13
# Subtest: agent runtime package routes
    # Subtest: queues one validated package profile for the unified agent image
    ok 1 - queues one validated package profile for the unified agent image
      ---
      duration_ms: 4.864405
      type: 'test'
      ...
    # Subtest: persists a failed state when queue submission fails
    ok 2 - persists a failed state when queue submission fails
      ---
      duration_ms: 0.97187
      type: 'test'
      ...
    # Subtest: rejects a package missing from an effective runtime before queueing
    ok 3 - rejects a package missing from an effective runtime before queueing
      ---
      duration_ms: 0.742022
      type: 'test'
      ...
    # Subtest: times out slow runtime package validation before queueing
    ok 4 - times out slow runtime package validation before queueing
      ---
      duration_ms: 7.7976
      type: 'test'
      ...
    # Subtest: returns package suggestions from configured runtimes
    ok 5 - returns package suggestions from configured runtimes
      ---
      duration_ms: 1.530394
      type: 'test'
      ...
    # Subtest: enforces PROPR_ADMIN_USERS when configured
    ok 6 - enforces PROPR_ADMIN_USERS when configured
      ---
      duration_ms: 0.5023
      type: 'test'
      ...
    # Subtest: denies runtime package changes by default when no admin policy is configured
    ok 7 - denies runtime package changes by default when no admin policy is configured
      ---
      duration_ms: 0.431943
      type: 'test'
      ...
    # Subtest: requires runtime-management permission before returning runtime package state
    ok 8 - requires runtime-management permission before returning runtime package state
      ---
      duration_ms: 0.576662
      type: 'test'
      ...
    # Subtest: warms the package catalog when an admin loads runtime package state
    ok 9 - warms the package catalog when an admin loads runtime package state
      ---
      duration_ms: 0.689742
      type: 'test'
      ...
    # Subtest: does not warm the package catalog when a member is rejected
    ok 10 - does not warm the package catalog when a member is rejected
      ---
      duration_ms: 13.499342
      type: 'test'
      ...
    # Subtest: resolves the runtime build queue lazily when queueing
    ok 11 - resolves the runtime build queue lazily when queueing
      ---
      duration_ms: 0.66187
      type: 'test'
      ...
    # Subtest: reports apply load failures through the route response
    ok 12 - reports apply load failures through the route response
      ---
      duration_ms: 0.348547
      type: 'test'
      ...
    # Subtest: verifies the current effective images through the runtime-management boundary
    ok 13 - verifies the current effective images through the runtime-management boundary
      ---
      duration_ms: 0.39668
      type: 'test'
      ...
    # Subtest: rejects runtime verification without runtime-management permission
    ok 14 - rejects runtime verification without runtime-management permission
      ---
      duration_ms: 0.25805
      type: 'test'
      ...
    1..14
ok 1 - agent runtime package routes
  ---
  duration_ms: 35.191151
  type: 'suite'
  ...
# [2026-09-03 09:50:45.000 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/home/runner/work/propr/propr/data/propr.test.sqlite"
#     environment: "test"
# [2026-09-03 09:50:45.032 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
# Subtest: upload handler removes Multer files rejected before attachment processing
ok 2 - upload handler removes Multer files rejected before attachment processing
  ---
  duration_ms: 9.922574
  type: 'test'
  ...
# Subtest: attachment processing removes temporary and final files when persistence fails
ok 3 - attachment processing removes temporary and final files when persistence fails
  ---
  duration_ms: 6.715525
  type: 'test'
  ...
# Subtest: temporary cleanup refuses paths outside its configured root
ok 4 - temporary cleanup refuses paths outside its configured root
  ---
  duration_ms: 0.91967
  type: 'test'
  ...
# Subtest: temporary cleanup rejects dot-segment paths that resolve above its root
ok 5 - temporary cleanup rejects dot-segment paths that resolve above its root
  ---
  duration_ms: 0.69467
  type: 'test'
  ...
# Subtest: attachment processing rejects path-like draft IDs and still cleans its temp file
ok 6 - attachment processing rejects path-like draft IDs and still cleans its temp file
  ---
  duration_ms: 3.063783
  type: 'test'
  ...
# [2026-09-03 09:50:44.580 +0000] �[32mINFO�[39m: �[36mCreated data directory for SQLite database�[39m
#     directory: "/home/runner/work/propr/propr/data"
# [2026-09-03 09:50:44.989 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/home/runner/work/propr/propr/data/propr.test.sqlite"
#     environment: "test"
# [2026-09-03 09:50:45.010 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
# Subtest: API config subscription serializes the startup reload with settings updates
ok 7 - API config subscription serializes the startup reload with settings updates
  ---
  duration_ms: 8.404828
  type: 'test'
  ...
# Subtest: API config subscription reloads repository updates
ok 8 - API config subscription reloads repository updates
  ---
  duration_ms: 0.519616
  type: 'test'
  ...
# Subtest: API config subscription ignores unrelated and malformed events
ok 9 - API config subscription ignores unrelated and malformed events
  ---
  duration_ms: 0.301757
  type: 'test'
  ...
# Subtest: a failed API settings reload does not block the next notification
ok 10 - a failed API settings reload does not block the next notification
  ---
  duration_ms: 0.407286
  type: 'test'
  ...
# Subtest: relay tunnel mode uses Connect without local OAuth credentials
ok 11 - relay tunnel mode uses Connect without local OAuth credentials
  ---
  duration_ms: 1.700761
  type: 'test'
  ...
# Subtest: relay tunnel mode preserves Connect for operator-supplied service endpoints
ok 12 - relay tunnel mode preserves Connect for operator-supplied service endpoints
  ---
  duration_ms: 0.29799
  type: 'test'
  ...
# Subtest: local relay mode uses Connect without a per-instance OAuth App
ok 13 - local relay mode uses Connect without a per-instance OAuth App
  ---
  duration_ms: 0.324711
  type: 'test'
  ...
# Subtest: off-tunnel relay inference rejects callbacks outside the exact loopback allowlist
ok 14 - off-tunnel relay inference rejects callbacks outside the exact loopback allowlist
  ---
  duration_ms: 0.426214
  type: 'test'
  ...
# Subtest: off-tunnel custom relay enrollment does not infer hosted Connect auth
ok 15 - off-tunnel custom relay enrollment does not infer hosted Connect auth
  ---
  duration_ms: 0.196688
  type: 'test'
  ...
# Subtest: literal example OAuth placeholders do not enable GitHub web auth
ok 16 - literal example OAuth placeholders do not enable GitHub web auth
  ---
  duration_ms: 0.160973
  type: 'test'
  ...
# Subtest: explicit custom GitHub web auth remains supported
ok 17 - explicit custom GitHub web auth remains supported
  ---
  duration_ms: 0.231089
  type: 'test'
  ...
# Subtest: explicit custom GitHub web auth wins over relay inference off-tunnel
ok 18 - explicit custom GitHub web auth wins over relay inference off-tunnel
  ---
  duration_ms: 0.205041
  type: 'test'
  ...
# Subtest: Connect authorization URL carries the exact callback and CSRF state
ok 19 - Connect authorization URL carries the exact callback and CSRF state
  ---
  duration_ms: 0.663884
  type: 'test'
  ...
# Subtest: redeems a Connect code server-to-server without exposing the relay token in the body
ok 20 - redeems a Connect code server-to-server without exposing the relay token in the body
  ---
  duration_ms: 55.186956
  type: 'test'
  ...
# Subtest: binds the Connect identity username to the validated token owner
ok 21 - binds the Connect identity username to the validated token owner
  ---
  duration_ms: 1.152733
  type: 'test'
  ...
# Subtest: accepts Docker IDs and ProPR-generated container names
ok 22 - accepts Docker IDs and ProPR-generated container names
  ---
  duration_ms: 1.230281
  type: 'test'
  ...
# Subtest: rejects container references that could be parsed as options or shell syntax
ok 23 - rejects container references that could be parsed as options or shell syntax
  ---
  duration_ms: 0.207814
  type: 'test'
  ...
# Subtest: production subprocess call sites do not invoke a command shell
ok 24 - production subprocess call sites do not invoke a command shell
  ---
  duration_ms: 150.660496
  type: 'test'
  ...
# Subtest: no-shell guard rejects aliases and enabled shell options
ok 25 - no-shell guard rejects aliases and enabled shell options
  ---
  duration_ms: 3.249593
  type: 'test'
  ...
# Subtest: instance authorization
    # Subtest: defaults unassigned authenticated users to members on a new installation
    ok 1 - defaults unassigned authenticated users to members on a new installation
      ---
      duration_ms: 15.540637
      type: 'test'
      ...
    # Subtest: refuses startup until a bootstrap or durable administrator exists
    ok 2 - refuses startup until a bootstrap or durable administrator exists
      ---
      duration_ms: 6.821478
      type: 'test'
      ...
    # Subtest: grants full admin permissions to PROPR_ADMIN_USERS
    ok 3 - grants full admin permissions to PROPR_ADMIN_USERS
      ---
      duration_ms: 3.466894
      type: 'test'
      ...
    # Subtest: resolves durable roles by numeric GitHub ID after a username change
    ok 4 - resolves durable roles by numeric GitHub ID after a username change
      ---
      duration_ms: 4.005238
      type: 'test'
      ...
    # Subtest: resolves demo users without installation permissions
    ok 5 - resolves demo users without installation permissions
      ---
      duration_ms: 3.024746
      type: 'test'
      ...
    # Subtest: permission middleware rejects members with a stable error code
    ok 6 - permission middleware rejects members with a stable error code
      ---
      duration_ms: 3.02815
      type: 'test'
      ...
    # Subtest: current-user responses expose capabilities without OAuth credentials
    ok 7 - current-user responses expose capabilities without OAuth credentials
      ---
      duration_ms: 2.443527
      type: 'test'
      ...
    1..7
ok 26 - instance authorization
  ---
  duration_ms: 42.27724
  type: 'suite'
  ...
# Subtest: instance member service
    # Subtest: does not let an unassigned user claim the initial administrator role
    ok 1 - does not let an unassigned user claim the initial administrator role
      ---
      duration_ms: 4.992251
      type: 'test'
      ...
    # Subtest: stores a bootstrap administrator against their numeric GitHub ID
    ok 2 - stores a bootstrap administrator against their numeric GitHub ID
      ---
      duration_ms: 10.541055
      type: 'test'
      ...
    # Subtest: adds explicit members and writes an audit entry
    ok 3 - adds explicit members and writes an audit entry
      ---
      duration_ms: 5.087165
      type: 'test'
      ...
    # Subtest: keeps environment authority separate from durable role assignments
    ok 4 - keeps environment authority separate from durable role assignments
      ---
      duration_ms: 3.673167
      type: 'test'
      ...
    # Subtest: prevents removing the last durable administrator
    ok 5 - prevents removing the last durable administrator
      ---
      duration_ms: 8.353015
      type: 'test'
      ...
    # Subtest: allows role changes once another administrator exists
    ok 6 - allows role changes once another administrator exists
      ---
      duration_ms: 10.795039
      type: 'test'
      ...
    1..6
ok 27 - instance member service
  ---
  duration_ms: 43.994558
  type: 'suite'
  ...
# Subtest: instance catalog
    # Subtest: returns operational agent and repository fields without installation internals
    ok 1 - returns operational agent and repository fields without installation internals
      ---
      duration_ms: 10.115361
      type: 'test'
      ...
    # Subtest: projects indexing status only for enabled repository and branch entries
    ok 2 - projects indexing status only for enabled repository and branch entries
      ---
      duration_ms: 2.806035
      type: 'test'
      ...
    1..2
ok 28 - instance catalog
  ---
  duration_ms: 13.070672
  type: 'suite'
  ...
# Subtest: instance admin routes
    # Subtest: resolves a GitHub username and creates one durable member assignment
    ok 1 - resolves a GitHub username and creates one durable member assignment
      ---
      duration_ms: 7.78774
      type: 'test'
      ...
    # Subtest: rejects a non-numeric audit limit with a 400
    ok 2 - rejects a non-numeric audit limit with a 400
      ---
      duration_ms: 2.501334
      type: 'test'
      ...
    # Subtest: rejects GitHub usernames with trailing or consecutive hyphens
    ok 3 - rejects GitHub usernames with trailing or consecutive hyphens
      ---
      duration_ms: 3.549629
      type: 'test'
      ...
    1..3
ok 29 - instance admin routes
  ---
  duration_ms: 14.000758
  type: 'suite'
  ...
# [2026-09-03 09:50:48.003 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/home/runner/work/propr/propr/data/propr.test.sqlite"
#     environment: "test"
# [2026-09-03 09:50:48.108 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
# Subtest: direct API execution binds to loopback by default
ok 30 - direct API execution binds to loopback by default
  ---
  duration_ms: 1.222499
  type: 'test'
  ...
# Subtest: containerized API execution remains reachable through its published port
ok 31 - containerized API execution remains reachable through its published port
  ---
  duration_ms: 0.222867
  type: 'test'
  ...
# Subtest: an explicit API listen host overrides environment defaults
ok 32 - an explicit API listen host overrides environment defaults
  ---
  duration_ms: 0.202857
  type: 'test'
  ...
# Subtest: GitHub OAuth authorization stores and sends a random state nonce
ok 33 - GitHub OAuth authorization stores and sends a random state nonce
  ---
  duration_ms: 3.630552
  type: 'test'
  ...
# Subtest: GitHub OAuth callback rejects a mismatched state before token exchange
ok 34 - GitHub OAuth callback rejects a mismatched state before token exchange
  ---
  duration_ms: 1.690648
  type: 'test'
  ...
# [2026-09-03 09:50:49.705 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/home/runner/work/propr/propr/data/propr.test.sqlite"
#     environment: "test"
# [2026-09-03 09:50:49.707 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
# Subtest: returns a standard 429 response after the configured quota
ok 35 - returns a standard 429 response after the configured quota
  ---
  duration_ms: 115.471384
  type: 'test'
  ...
# Subtest: does not charge CORS preflight requests against the quota
ok 36 - does not charge CORS preflight requests against the quota
  ---
  duration_ms: 14.350687
  type: 'test'
  ...
# Subtest: route-level webhook limiting preserves alternate-case raw bodies and rejects excess requests before parsing
ok 37 - route-level webhook limiting preserves alternate-case raw bodies and rejects excess requests before parsing
  ---
  duration_ms: 36.768598
  type: 'test'
  ...
# Subtest: does not let an unconfigured private peer rotate quota buckets with X-Forwarded-For
ok 38 - does not let an unconfigured private peer rotate quota buckets with X-Forwarded-For
  ---
  duration_ms: 18.992579
  type: 'test'
  ...
# Subtest: tunnel trust does not let an unrelated private peer rotate quota buckets
ok 39 - tunnel trust does not let an unrelated private peer rotate quota buckets
  ---
  duration_ms: 24.597477
  type: 'test'
  ...
# Subtest: trusted TLS proxy preserves per-client quotas and secure session cookies
ok 40 - trusted TLS proxy preserves per-client quotas and secure session cookies
  ---
  duration_ms: 20.289441
  type: 'test'
  ...
# Subtest: resolves secure defaults and explicit positive-integer overrides
ok 41 - resolves secure defaults and explicit positive-integer overrides
  ---
  duration_ms: 0.541429
  type: 'test'
  ...
# Subtest: rejects invalid overrides instead of silently disabling protection
ok 42 - rejects invalid overrides instead of silently disabling protection
  ---
  duration_ms: 1.112663
  type: 'test'
  ...
# Subtest: assembled instance permission routes
    # Subtest: captures Express 5 named wildcard parameters as path segments
    ok 1 - captures Express 5 named wildcard parameters as path segments
      ---
      duration_ms: 44.851574
      type: 'test'
      ...
    # Subtest: members can read only the sanitized catalog endpoints
    ok 2 - members can read only the sanitized catalog endpoints
      ---
      duration_ms: 69.675139
      type: 'test'
      ...
    # Subtest: administrators pass every management guard in the route matrix
    ok 3 - administrators pass every management guard in the route matrix
      ---
      duration_ms: 34.394296
      type: 'test'
      ...
    # Subtest: demo users can read only the synthetic Agent Tank usage feed
    ok 4 - demo users can read only the synthetic Agent Tank usage feed
      ---
      duration_ms: 22.658595
      type: 'test'
      ...
    1..4
ok 43 - assembled instance permission routes
  ---
  duration_ms: 173.279233
  type: 'suite'
  ...
# [2026-09-03 09:50:50.024 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/home/runner/work/propr/propr/data/propr.test.sqlite"
#     environment: "test"
# [2026-09-03 09:50:50.191 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status omits disabled configured agents
ok 44 - /api/status omits disabled configured agents
  ---
  duration_ms: 1977.256823
  type: 'test'
  ...
# Subtest: /api/compatibility returns public version contract metadata
ok 45 - /api/compatibility returns public version contract metadata
  ---
  duration_ms: 3.297548
  type: 'test'
  ...
# Subtest: /api/status returns default Claude fallback when no agents are configured
ok 46 - /api/status returns default Claude fallback when no agents are configured
  ---
  duration_ms: 3.874611
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# [notifications] Failed to project system health snapshot: Error: notification persistence unavailable
#     at projectSystemSnapshot (/home/runner/work/propr/propr/packages/api/test/statusRoutes.test.ts:229:13)
#     at Object.getStatus (/home/runner/work/propr/propr/packages/api/routes/statusRoutes.ts:194:14)
#     at async readStatus (/home/runner/work/propr/propr/packages/api/test/statusRoutes.test.ts:164:3)
#     at async TestContext.<anonymous> (/home/runner/work/propr/propr/packages/api/test/statusRoutes.test.ts:226:16)
#     at async Test.run (node:internal/test_runner/test:1054:7)
#     at async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
# Subtest: /api/status isolates system notification projection failures
ok 47 - /api/status isolates system notification projection failures
  ---
  duration_ms: 8.862747
  type: 'test'
  ...
# Subtest: /api/status surfaces unified agent image outages
ok 48 - /api/status surfaces unified agent image outages
  ---
  duration_ms: 1.754323
  type: 'test'
  ...
# Subtest: /api/status includes warnings field in demo mode
ok 49 - /api/status includes warnings field in demo mode
  ---
  duration_ms: 1.080604
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status caches agent health checks briefly
ok 50 - /api/status caches agent health checks briefly
  ---
  duration_ms: 4.51515
  type: 'test'
  ...
# Subtest: /api/status marks an unavailable synthetic pool degraded without downgrading direct agents
ok 51 - /api/status marks an unavailable synthetic pool degraded without downgrading direct agents
  ---
  duration_ms: 4.485455
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status reports resolved auth mode and event intake mode
ok 52 - /api/status reports resolved auth mode and event intake mode
  ---
  duration_ms: 3.88087
  type: 'test'
  ...
# Subtest: /api/status reports connected intake status when routing state is live
ok 53 - /api/status reports connected intake status when routing state is live
  ---
  duration_ms: 3.899388
  type: 'test'
  ...
# Subtest: /api/status reports disconnected intake status when routing state is down
ok 54 - /api/status reports disconnected intake status when routing state is down
  ---
  duration_ms: 3.216636
  type: 'test'
  ...
# Subtest: /api/status reports active intake status for polling when the daemon is running
ok 55 - /api/status reports active intake status for polling when the daemon is running
  ---
  duration_ms: 2.581765
  type: 'test'
  ...
# Subtest: /api/status reports disconnected intake status for polling when the daemon is stopped
ok 56 - /api/status reports disconnected intake status for polling when the daemon is stopped
  ---
  duration_ms: 1.856939
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status includes routing state published by the daemon
ok 57 - /api/status includes routing state published by the daemon
  ---
  duration_ms: 2.611039
  type: 'test'
  ...
# Subtest: /api/status exposes only validated UI-safe Connect account fields
ok 58 - /api/status exposes only validated UI-safe Connect account fields
  ---
  duration_ms: 2.44619
  type: 'test'
  ...
# Subtest: /api/status rejects impossible account dates and preserves valid leap-day instants
ok 59 - /api/status rejects impossible account dates and preserves valid leap-day instants
  ---
  duration_ms: 6.885566
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/prop

...(truncated)

View Workflow

@integry integry added the AI label Sep 3, 2026
@integry

integry commented Sep 3, 2026

Copy link
Copy Markdown
Owner

/review opus

@propr-dev

propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

AI Code Review Complete requested by @integry

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — opus

Overall Evaluation

This PR introduces a branch-independent, opt-in "visual preview" policy: repository-level configuration (UI + CLI + API validation + core resolver), an agent prompt addendum, a collector that turns committed .propr/previews/** media into structured evidence, a markdown renderer with commit-pinned fallbacks, and a gh-based attachment publisher for PR bodies and follow-up completion comments. It also pins GitHub CLI 2.99 into both the agent and production images and adds documentation. The layering is coherent, the surface area is well-partitioned, and test coverage for the new leaf modules is genuinely good.

Defensive asset collectionpackages/core/src/services/visualPreviewService.ts normalizes and rejects traversal paths, uses lstat to exclude symlinks and non-regular files, drops zero-byte files, caps count/size/manifest bytes, and only accepts files that appear in the current commit's filesChanged. The accompanying test asserts the symlink, out-of-tree, empty-file, and type-filter cases.

Credential hygiene in the publishersrc/github/visualPreviewAttachments.ts passes the installation token via GH_TOKEN rather than argv, and test/visualPreviewAttachments.test.ts explicitly asserts the token never appears in the command arguments.

Layered, tested failure recovery — Attachment failures fall back to commit-pinned ?raw=true links in both the PR (issueJobHelpers.ts) and the completion comment (prCommentPostExecution.ts), and a partially-published attachment comment is deleted before the error is rethrown, with a dedicated test covering that path.

One PR-introduced regression needs correcting before merge: the "no preview section" path of appendVisualPreviewSection rewrites the entire PR/comment body rather than just removing its placeholder, and that path executes for every repository — including the overwhelming majority that will never enable this feature. The fix is a one-line scope reduction. Everything else I found is non-blocking.

Merge blockers

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

F1: 🔴 Empty-section path rewrites unrelated PR and comment bodies

  • Required behavior: Visual previews are documented and implemented as opt-in ("The feature is opt-in per repository. Existing repository configurations remain disabled after an upgrade." — docs/docs/features/visual-previews.md). Publishing behavior for repositories with previews disabled must be unchanged by this PR.
  • Evidence: packages/core/src/services/visualPreviewService.tsappendVisualPreviewSection returns body.replace(VISUAL_PREVIEW_SLOT, '').replace(/\n{3,}/g, '\n\n').trim() whenever section is empty. That branch is reached unconditionally on both publish paths: src/jobs/issueJobHelpers.ts computes const prBody = appendVisualPreviewSection(basePrBody, visualPreviewSection); where visualPreviewSection is '' whenever evidence is empty (i.e. always, when previews are disabled), and src/jobs/prCommentPostExecution.ts computes const prCommentBody = appendVisualPreviewSection(prCommentTemplate, visualPreviewSection); the same way. The global /\n{3,}/g collapse and .trim() therefore mutate every generated PR description and every follow-up completion comment, rewriting agent-authored changesSummary/commitMessage content — including blank lines inside fenced code blocks, where line spacing is semantically part of the rendered output.
  • Minimum fix: In the empty-section branch, remove only the placeholder and its immediately adjacent blank line instead of normalizing the whole document — e.g. return body.includes(VISUAL_PREVIEW_SLOT) ? body.replace(new RegExp(\n*${escapeRegExp(VISUAL_PREVIEW_SLOT)}\n*), '\n\n') : body; (or simply body.replace(VISUAL_PREVIEW_SLOT, '') with no global whitespace pass), leaving bodies without a slot byte-identical to their pre-PR form.

Suggestions

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

S1: 🟢 Escape $ in replacement strings

appendVisualPreviewSection uses body.replace(VISUAL_PREVIEW_SLOT, section) with section as a string replacement. JavaScript interprets $&, $`, $', and $$ inside string replacements, and markdownText in visualPreviewService.ts escapes many punctuation characters but not $. Since asset titles and descriptions originate from the agent-written .propr/previews/manifest.json, a title containing $' would splice the remainder of the body into the section. The blast radius is a mangled PR body rather than a security issue, and the trigger is unlikely, so this is optional — but switching to a replacer function (body.replace(SLOT, () => section)) removes the class of bug entirely and costs nothing.

S2: 🟢 Verify and validate the gh --attach upload

The whole attachment path in src/github/visualPreviewAttachments.ts depends on gh pr edit/gh pr comment accepting --attach and rewriting the local absolute paths embedded in --body into uploaded asset URLs. If the flag does not exist the command errors and the commit-pinned fallback engages cleanly, which is good design. The residual risk is the middle case: a gh build that accepts the flag but leaves the body untouched would be treated as success, publishing a PR body or completion comment containing broken /tmp/pr-worktrees/... image links — and in the comment path the original "starting work" comment has already been deleted, so the good fallback content is gone. A cheap post-condition (the comment path already fetches the published body via GET /issues/comments/{comment_id}; the PR path fetches nothing) asserting the published body no longer contains worktreePath would convert that into a normal fallback. It also avoids incidentally disclosing internal host paths in public GitHub content.

S3: 🟢 De-duplicate the settings normalizer

normalizeStoredVisualPreviewSettings now exists twice with identical semantics: in packages/core/src/config/configManager.ts and, copied, in packages/api/routes/configRepoValidation.ts. The inline comment explains the motivation (avoiding a runtime @propr/core import that initializes GitHub auth), which is reasonable, but the two copies will drift as the settings shape evolves and only the core copy is unit-tested (packages/core/test/visualPreviewConfig.test.ts). Extracting the pure normalizer into a side-effect-free module (e.g. @propr/shared) that both import would keep the constraint and remove the duplication.

S4: 🟢 Preserve diagnostics from failed gh invocations

runAttachmentCommand replaces the execa error with a generic new Error('GitHub CLI could not upload visual preview attachments'), keeping only stdout. The exit code and stderr — where gh reports unknown flags, auth scope failures, and per-file upload errors — are dropped, so the only operator-visible signal is the warning logged in issueJobHelpers.ts/prCommentPostExecution.ts. Attaching a sanitized stderr tail and the exit code to the wrapped error would make the difference between "flag unsupported" and "file too large" diagnosable without reproducing. Relatedly, the fixed timeout: 60_000 covers up to 8 × 10 MB uploads, which may be tight on constrained links; the failure is graceful, so this is tuning rather than a defect.

S5: 🟢 Test the job-level preview wiring

The new leaf modules are well covered, but the two orchestration functions this PR added are not: publishCompletionComment in src/jobs/prCommentPostExecution.ts (slot insertion, hasVisualPreviewContent gating, attachment-failure fallback to PATCH) and the visual-preview branch of createPullRequest in src/jobs/issueJobHelpers.ts (upload attempt plus body-restore on failure). These are where the interesting branching lives — for example the case where the attachment succeeds but the GET of the published body fails, which currently returns options.body still containing the raw VISUAL_PREVIEW_SLOT marker into task history. Tests here would lock in the fallback contract that the docs promise.

S6: 🟢 Tighten asset selection and suggestion budgets

Two small ordering issues in collectVisualPreviewEvidence: MAX_PREVIEW_ASSETS is applied via .slice(0, 8) before files are validated, so eight invalid or oversized candidates can crowd out valid ones later in sort order; and the oversized-media hint is pushed after the manifest suggestions were already capped at five, then slice(0, 5) runs again — so with five manifest suggestions the actionable "install ffmpeg" hint is silently dropped, which is exactly the signal the objective wants surfaced. Filtering before the cap and reserving a slot for the synthesized suggestion would both be small changes.

S7: 🟢 Visual preview control UX details

Two minor UI points in propr-ui/src/components/RepositoryVisualPreviewControl.tsx: the component returns null when isReadOnly, so read-only members cannot see whether previews are enabled for a repository (the auto-CI control next to it stays visible in disabled form); and useEffect(() => setInstructions(settings.instructions || ''), [settings.instructions]) will reset the textarea if the repository list refreshes while a user is mid-edit, discarding uncommitted typing. Neither blocks the feature, but both are cheap to address (render disabled instead of hiding; track a dirty flag before resyncing).

Score

The design is sound, the fallback layering is thoughtful, coverage of the new leaf modules is strong, and all head checks pass; the single blocker is a narrow, one-line scope error in appendVisualPreviewSection that silently rewrites PR and comment bodies for repositories that never opted into the feature.

Score: 6/10


🤖 Review Details

  • Model: Claude Opus 5
  • Time: 5m 12s
  • Tokens: 119,804 (95,243 in / 24,561 out)
  • Cost: $1.21

View Task


💡 Next step: Comment /fix to address F# merge blockers only.
F# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. Suggestions require a separate ordinary follow-up request.

🤖 Review by ProPR

Repository owner deleted a comment from propr-dev Bot Sep 3, 2026
@integry

integry commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Don't commit the preview files, they can attached to PRs directly

• Yes—GitHub CLI 2.99 makes this practical. It supports --attach on both gh pr edit and gh pr comment, producing GitHub-hosted user-attachments URLs without storing media in Git. GitHub’s announcement
(https://github.blog/changelog/2026-09-01-github-cli-media-in-issues-pull-requests-and-comments/)

PR #2102 currently still commits previews because:

  • Agents write to .propr/previews/.
  • commitChanges() runs git add ..
  • Evidence is collected only after committing, using commitResult.filesChanged.

I’d change the lifecycle to:

Agent generates preview

Collect and validate before committing

Copy preview to worker-owned temporary directory

Remove .propr/previews from worktree

Commit and push code only

gh pr edit/comment --attach

Delete temporary directory

Concretely:

  • Treat .propr/previews/ as transient runtime output, like .propr/cache.
  • Collect its manifest and files before commitChanges().
  • Copy accepted files to something like /tmp/propr-previews//.
  • Remove/reset the preview directory before git add ..
  • Pass the copied absolute paths into the existing publishPullRequestVisualPreviews() and publishPullRequestCommentVisualPreviews().
  • On upload failure, publish a text-only explanation. The current commit-pinned fallback would no longer be possible.
  • After upload, fetch the resulting body and assert no local filesystem paths remain.
  • Always clean up the temporary directory in finally.

A cleaner later design would expose a worker-managed directory such as PROPR_VISUAL_PREVIEW_DIR inside the agent container, outside the repository from the start. That removes the need to scrub the worktree.

One item needs real integration validation: #2102 passes a GitHub App installation token through GH_TOKEN, while GitHub’s documentation explicitly mentions OAuth and classic PAT authentication. I’d test an actual ProPR App upload before relying on it.

Also, the current head still contains the review blocker in appendVisualPreviewSection(): its empty-preview path globally normalizes whitespace in unrelated PR/comment bodies. That should be fixed independently.

@propr-dev

propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Applied the requested follow-up changes in commit 95e0e05

Summary of Changes

Implemented the PR #2102 follow-up without committing changes.

Key changes:

  • Preview evidence is collected before commit, copied to /tmp/propr-previews/<task-id>-*/, and removed/reset from the worktree.
  • .propr/previews is excluded defensively from commitChanges().
  • Temporary files are always cleaned up after publication.
  • PR and comment uploads fetch the resulting GitHub body and reject leaked local paths.
  • Upload failures now publish text-only explanations; commit-pinned fallbacks were removed.
  • Fixed appendVisualPreviewSection() so empty previews no longer normalize unrelated whitespace.
  • Updated tests and visual preview documentation.

Validation passed:

  • 26 focused/lifecycle tests
  • Root and core typechecks
  • Root and core lint
  • Root and core builds
  • git diff --check
  • 146 related helper/git-hook tests

A live GitHub App upload was not performed: the environment has GitHub CLI 2.96.0, not 2.99, and directly modifying PR #2102 is outside the allowed actions.


🤖 Implementation Details

  • Model: GPT-5.6 Sol (high)
  • Requested By: @integry
  • Time: 15m 30s
  • Tokens: 4,960,439 (4,929,814 in / 30,625 out)
  • Cost: $4.16

Undo ChangesView 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 Implement fixes for issues found by /review /fix or /fix address the null check issue
/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: 5525114134✓

Implemented the PR #2102 follow-up without committing changes.

Key changes:

- Preview evidence is collected before commit, copied to `/tmp/propr-previews/<task-id>-*/`, and removed/reset from the worktree.
- `.propr/previews` is excluded defensively from `commitChanges()`.
- Temporary files are always cleaned up after publication.
- PR and comment uploads fetch the resulting GitHub body and reject leaked local paths.
- Upload failures now publish text-only explanations; commit-pinned fallbacks were removed.
- Fixed `appendVisualPreviewSection()` so empty previews no longer normalize unrelated whitespace.
- Updated tests and [visual preview documentation](/tmp/git-processor/worktrees/integry/propr/pr-2102-followup-2026-09-03T11-36-55/docs/docs/features/visual-previews.md).

Validation passed:

- 26 focused/lifecycle tests
- Root and core typechecks
- Root and core lint
- Root and core builds
- `git diff --check`
- 146 related helper/git-hook tests

A live GitHub App upload was not performed: the environment has GitHub CLI 2.96.0, not 2.99, and directly modifying PR #2102 is outside the allowed actions.

PR: #2102
Comment by: @integry (ID: 5525114134)
Model: gpt-5.6-sol
Comment thread packages/core/src/services/visualPreviewService.ts Fixed
@integry

integry commented Sep 3, 2026

Copy link
Copy Markdown
Owner

github-advanced-security Bot found potential problems 33 minutes ago
View reviewed changes

packages/core/src/services/visualPreviewService.ts
}

function safeTemporaryName(taskId: string): string {
const normalized = taskId.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80);
github-advanced-security commented 33 minutes ago

github-advanced-security
bot
33m ago
High

CodeQL / Polynomial regular expression used on uncontrolled data
This regular expression that depends on library input may run slow on strings with many repetitions of '-'.

Show more details

@propr-dev

propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Applied the requested follow-up changes in commit 37113e7

Summary of Changes

Updated visualPreviewService.ts.

Replaced the CodeQL-flagged trimming regex with linear index scans while preserving normalization behavior and the 80-character limit.

Validation passed:

  • 7 focused visual-preview tests
  • Core typecheck
  • Core lint
  • git diff --check

No commit created.


🤖 Implementation Details

  • Model: GPT-5.6 Sol (high)
  • Requested By: @integry
  • Time: 2m 11s
  • Tokens: 213,103 (210,700 in / 2,403 out)
  • Cost: $0.32

Undo ChangesView 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 Implement fixes for issues found by /review /fix or /fix address the null check issue
/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: 5525753130✓

@integry

integry commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Also upgrade gh to the latest version, here's the release info which is relevant:

Release list
GitHub CLI 2.99.0
GitHub CLI 2.99.0 Latest
@github-actions github-actions released this 2 days ago
Immutablerelease. Only release title and notes can be modified.
v2.99.0
cli/cli@d528f20
Attach images and videos to issues and pull requests
The repeatable --attach flag uploads local images and videos and adds them to issue, pull request, or comment bodies. If a body already references the local path, gh replaces it with the uploaded URL; otherwise it appends the attachment:

Attach files when creating or editing an issue

gh issue create --attach './repro.png#The error state'
gh issue edit 123 --attach ./walkthrough.mp4

Attach files when creating or editing a pull request

gh pr create --attach ./before.png
gh pr edit 456 --attach ./after.png

Attach files to comments

gh issue comment 123 --attach ./repro.png
gh pr comment 456 --attach ./result.mp4
Repeat the flag to attach multiple files in a single invocation. Attachments are available on GitHub.com and GitHub Enterprise Cloud.

For more information see https://gh.io/gh-attach and https://github.blog/changelog/2026-09-01-github-cli-media-in-issues-pull-requests-and-comments/

Worktree support extended to gh issue develop
gh issue develop can now create a linked branch and check it out in a new Git worktree, leaving your current working copy unchanged:

Create a linked branch for an issue and check it out in a worktree

gh issue develop 123 --checkout --worktree /path/to/wt-feature
What's Changed

…ees/integry/propr/pr-2102-followup-2026-09-03T12-28-37/packages/core/src/services/visualPreviewService.ts:217).

Updated [visualPreviewService.ts](/tmp/git-processor/worktrees/integry/propr/pr-2102-followup-2026-09-03T12-28-37/packages/core/src/services/visualPreviewService.ts:217).

Replaced the CodeQL-flagged trimming regex with linear index scans while preserving normalization behavior and the 80-character limit.

Validation passed:

- 7 focused visual-preview tests
- Core typecheck
- Core lint
- `git diff --check`

No commit created.

PR: #2102
Comment by: @integry (ID: 5525753130)
Model: gpt-5.6-sol
@propr-dev

propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

ℹ️ Analyzed the follow-up request by @integry

Analysis Summary

The requested upgrade is already present on this PR branch, so no additional edits were needed:

  • Dockerfile.agent targets the 2.99 series.
  • Dockerfile.app.prod pins gh to 2.99.0 and verifies official checksums.
  • Confirmed official amd64 and arm64 release assets exist.
  • Focused tests: 12 passed.
  • Worktree remains clean.

No code changes were necessary based on the current state of the branch.


🤖 Analysis Details

  • Model: GPT-5.6 Sol (high)
  • Time: 2m 16s
  • Tokens: 221,922 (218,659 in / 3,263 out)
  • Cost: $0.36

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 Implement fixes for issues found by /review /fix or /fix address the null check issue
/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: 5525779180✓

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Checks Failed

Validation failed during setup, tests, CLI packaging, linting, or build checks.

View Logs
Build check diagnostics for run 33773572634, attempt 1
Checkout outcome: success

--- GitHub Actions workflow lint ---
docker.io/rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667: Pulling from rhysd/actionlint
589002ba0eae: Pulling fs layer
c09af8888d6a: Pulling fs layer
4ccd7d76ca98: Pulling fs layer
b6b4b7b0e257: Pulling fs layer
b6b4b7b0e257: Waiting
4ccd7d76ca98: Verifying Checksum
4ccd7d76ca98: Download complete
c09af8888d6a: Verifying Checksum
c09af8888d6a: Download complete
589002ba0eae: Verifying Checksum
589002ba0eae: Download complete
b6b4b7b0e257: Verifying Checksum
b6b4b7b0e257: Download complete
589002ba0eae: Pull complete
c09af8888d6a: Pull complete
4ccd7d76ca98: Pull complete
b6b4b7b0e257: Pull complete
Digest: sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667
Status: Downloaded newer image for rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667
docker.io/rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667

--- Release shell script lint ---

--- Toolchain ---
v22.23.2
10.9.8

--- Dependency installation ---

added 979 packages, and audited 985 packages in 18s

297 packages are looking for funding
  run `npm fund` for details

3 vulnerabilities (2 moderate, 1 high)

To address all issues, run:
  npm audit fix

Run `npm audit` for details.

--- Fast unit tests ---
Running fast unit tests (no Redis/Docker dependencies)...

> propr@0.8.15 test:unit
> NODE_ENV=test npx tsx --experimental-test-module-mocks --test test/minimal.test.ts test/modelName.test.ts test/agentContainerResources.test.ts test/agentDockerfileSupplyChain.test.ts test/agentImagePreparation.test.ts test/daemonEventIntake.test.ts test/databaseMigrationGate.test.ts test/deployPrPreview.test.mjs test/generateContext.test.ts test/githubEventIntakeMode.test.ts test/intakeModePrerequisites.test.ts test/orchestratorMigrationPhase.test.mjs test/validateRoutingUrl.test.ts test/routingWebSocketProtocol.test.ts test/routingWebSocketIntakeService.test.ts test/routingStatusPublisher.test.ts test/releaseValidation.test.mjs test/sessionSecret.test.ts test/testSuiteRunner.test.mjs packages/api/test/connectAuth.test.ts packages/api/test/attachmentUploadCleanup.test.ts packages/api/test/configReloadSubscription.test.ts packages/api/test/dockerCommandSafety.test.ts packages/api/test/listenAddress.test.ts packages/api/test/oauthState.test.ts packages/api/test/requestRateLimits.test.ts packages/api/test/statusRoutes.test.ts packages/api/test/agentRuntimeRoutes.test.ts packages/api/test/instanceAuthorization.test.ts packages/api/test/routeAuthorization.test.ts

TAP version 13
# Subtest: agent runtime package routes
    # Subtest: queues one validated package profile for the unified agent image
    ok 1 - queues one validated package profile for the unified agent image
      ---
      duration_ms: 11.524766
      type: 'test'
      ...
    # Subtest: persists a failed state when queue submission fails
    ok 2 - persists a failed state when queue submission fails
      ---
      duration_ms: 1.470292
      type: 'test'
      ...
    # Subtest: rejects a package missing from an effective runtime before queueing
    ok 3 - rejects a package missing from an effective runtime before queueing
      ---
      duration_ms: 1.693581
      type: 'test'
      ...
    # Subtest: times out slow runtime package validation before queueing
    ok 4 - times out slow runtime package validation before queueing
      ---
      duration_ms: 20.048005
      type: 'test'
      ...
    # Subtest: returns package suggestions from configured runtimes
    ok 5 - returns package suggestions from configured runtimes
      ---
      duration_ms: 1.062673
      type: 'test'
      ...
    # Subtest: enforces PROPR_ADMIN_USERS when configured
    ok 6 - enforces PROPR_ADMIN_USERS when configured
      ---
      duration_ms: 0.483491
      type: 'test'
      ...
    # Subtest: denies runtime package changes by default when no admin policy is configured
    ok 7 - denies runtime package changes by default when no admin policy is configured
      ---
      duration_ms: 0.393299
      type: 'test'
      ...
    # Subtest: requires runtime-management permission before returning runtime package state
    ok 8 - requires runtime-management permission before returning runtime package state
      ---
      duration_ms: 0.500807
      type: 'test'
      ...
    # Subtest: warms the package catalog when an admin loads runtime package state
    ok 9 - warms the package catalog when an admin loads runtime package state
      ---
      duration_ms: 0.790883
      type: 'test'
      ...
    # Subtest: does not warm the package catalog when a member is rejected
    ok 10 - does not warm the package catalog when a member is rejected
      ---
      duration_ms: 10.430748
      type: 'test'
      ...
    # Subtest: resolves the runtime build queue lazily when queueing
    ok 11 - resolves the runtime build queue lazily when queueing
      ---
      duration_ms: 7.737117
      type: 'test'
      ...
    # Subtest: reports apply load failures through the route response
    ok 12 - reports apply load failures through the route response
      ---
      duration_ms: 0.622406
      type: 'test'
      ...
    # Subtest: verifies the current effective images through the runtime-management boundary
    ok 13 - verifies the current effective images through the runtime-management boundary
      ---
      duration_ms: 0.736193
      type: 'test'
      ...
    # Subtest: rejects runtime verification without runtime-management permission
    ok 14 - rejects runtime verification without runtime-management permission
      ---
      duration_ms: 0.41485
      type: 'test'
      ...
    1..14
ok 1 - agent runtime package routes
  ---
  duration_ms: 60.459648
  type: 'suite'
  ...
# [2026-09-03 15:36:37.707 +0000] �[32mINFO�[39m: �[36mCreated data directory for SQLite database�[39m
#     directory: "/home/runner/work/propr/propr/data"
# [2026-09-03 15:36:38.216 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/home/runner/work/propr/propr/data/propr.test.sqlite"
#     environment: "test"
# [2026-09-03 15:36:38.272 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
# Subtest: upload handler removes Multer files rejected before attachment processing
ok 2 - upload handler removes Multer files rejected before attachment processing
  ---
  duration_ms: 11.347736
  type: 'test'
  ...
# Subtest: attachment processing removes temporary and final files when persistence fails
ok 3 - attachment processing removes temporary and final files when persistence fails
  ---
  duration_ms: 7.176563
  type: 'test'
  ...
# Subtest: temporary cleanup refuses paths outside its configured root
ok 4 - temporary cleanup refuses paths outside its configured root
  ---
  duration_ms: 0.922457
  type: 'test'
  ...
# Subtest: temporary cleanup rejects dot-segment paths that resolve above its root
ok 5 - temporary cleanup rejects dot-segment paths that resolve above its root
  ---
  duration_ms: 0.734189
  type: 'test'
  ...
# Subtest: attachment processing rejects path-like draft IDs and still cleans its temp file
ok 6 - attachment processing rejects path-like draft IDs and still cleans its temp file
  ---
  duration_ms: 4.071898
  type: 'test'
  ...
# [2026-09-03 15:36:38.196 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/home/runner/work/propr/propr/data/propr.test.sqlite"
#     environment: "test"
# [2026-09-03 15:36:38.219 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
# Subtest: API config subscription serializes the startup reload with settings updates
ok 7 - API config subscription serializes the startup reload with settings updates
  ---
  duration_ms: 12.660347
  type: 'test'
  ...
# Subtest: API config subscription reloads repository updates
ok 8 - API config subscription reloads repository updates
  ---
  duration_ms: 0.668794
  type: 'test'
  ...
# Subtest: API config subscription ignores unrelated and malformed events
ok 9 - API config subscription ignores unrelated and malformed events
  ---
  duration_ms: 3.541628
  type: 'test'
  ...
# Subtest: a failed API settings reload does not block the next notification
ok 10 - a failed API settings reload does not block the next notification
  ---
  duration_ms: 0.63236
  type: 'test'
  ...
# Subtest: relay tunnel mode uses Connect without local OAuth credentials
ok 11 - relay tunnel mode uses Connect without local OAuth credentials
  ---
  duration_ms: 1.683336
  type: 'test'
  ...
# Subtest: relay tunnel mode preserves Connect for operator-supplied service endpoints
ok 12 - relay tunnel mode preserves Connect for operator-supplied service endpoints
  ---
  duration_ms: 0.440969
  type: 'test'
  ...
# Subtest: local relay mode uses Connect without a per-instance OAuth App
ok 13 - local relay mode uses Connect without a per-instance OAuth App
  ---
  duration_ms: 0.438635
  type: 'test'
  ...
# Subtest: off-tunnel relay inference rejects callbacks outside the exact loopback allowlist
ok 14 - off-tunnel relay inference rejects callbacks outside the exact loopback allowlist
  ---
  duration_ms: 0.432536
  type: 'test'
  ...
# Subtest: off-tunnel custom relay enrollment does not infer hosted Connect auth
ok 15 - off-tunnel custom relay enrollment does not infer hosted Connect auth
  ---
  duration_ms: 3.27804
  type: 'test'
  ...
# Subtest: literal example OAuth placeholders do not enable GitHub web auth
ok 16 - literal example OAuth placeholders do not enable GitHub web auth
  ---
  duration_ms: 0.272441
  type: 'test'
  ...
# Subtest: explicit custom GitHub web auth remains supported
ok 17 - explicit custom GitHub web auth remains supported
  ---
  duration_ms: 0.283638
  type: 'test'
  ...
# Subtest: explicit custom GitHub web auth wins over relay inference off-tunnel
ok 18 - explicit custom GitHub web auth wins over relay inference off-tunnel
  ---
  duration_ms: 0.327942
  type: 'test'
  ...
# Subtest: Connect authorization URL carries the exact callback and CSRF state
ok 19 - Connect authorization URL carries the exact callback and CSRF state
  ---
  duration_ms: 0.733859
  type: 'test'
  ...
# Subtest: redeems a Connect code server-to-server without exposing the relay token in the body
ok 20 - redeems a Connect code server-to-server without exposing the relay token in the body
  ---
  duration_ms: 153.781648
  type: 'test'
  ...
# Subtest: binds the Connect identity username to the validated token owner
ok 21 - binds the Connect identity username to the validated token owner
  ---
  duration_ms: 5.684188
  type: 'test'
  ...
# Subtest: accepts Docker IDs and ProPR-generated container names
ok 22 - accepts Docker IDs and ProPR-generated container names
  ---
  duration_ms: 1.239342
  type: 'test'
  ...
# Subtest: rejects container references that could be parsed as options or shell syntax
ok 23 - rejects container references that could be parsed as options or shell syntax
  ---
  duration_ms: 0.250338
  type: 'test'
  ...
# Subtest: production subprocess call sites do not invoke a command shell
ok 24 - production subprocess call sites do not invoke a command shell
  ---
  duration_ms: 171.232875
  type: 'test'
  ...
# Subtest: no-shell guard rejects aliases and enabled shell options
ok 25 - no-shell guard rejects aliases and enabled shell options
  ---
  duration_ms: 11.346284
  type: 'test'
  ...
# Subtest: instance authorization
    # Subtest: defaults unassigned authenticated users to members on a new installation
    ok 1 - defaults unassigned authenticated users to members on a new installation
      ---
      duration_ms: 16.323771
      type: 'test'
      ...
    # Subtest: refuses startup until a bootstrap or durable administrator exists
    ok 2 - refuses startup until a bootstrap or durable administrator exists
      ---
      duration_ms: 7.63845
      type: 'test'
      ...
    # Subtest: grants full admin permissions to PROPR_ADMIN_USERS
    ok 3 - grants full admin permissions to PROPR_ADMIN_USERS
      ---
      duration_ms: 4.167863
      type: 'test'
      ...
    # Subtest: resolves durable roles by numeric GitHub ID after a username change
    ok 4 - resolves durable roles by numeric GitHub ID after a username change
      ---
      duration_ms: 4.007167
      type: 'test'
      ...
    # Subtest: resolves demo users without installation permissions
    ok 5 - resolves demo users without installation permissions
      ---
      duration_ms: 3.159129
      type: 'test'
      ...
    # Subtest: permission middleware rejects members with a stable error code
    ok 6 - permission middleware rejects members with a stable error code
      ---
      duration_ms: 6.201816
      type: 'test'
      ...
    # Subtest: current-user responses expose capabilities without OAuth credentials
    ok 7 - current-user responses expose capabilities without OAuth credentials
      ---
      duration_ms: 3.027054
      type: 'test'
      ...
    1..7
ok 26 - instance authorization
  ---
  duration_ms: 46.337197
  type: 'suite'
  ...
# Subtest: instance member service
    # Subtest: does not let an unassigned user claim the initial administrator role
    ok 1 - does not let an unassigned user claim the initial administrator role
      ---
      duration_ms: 26.743634
      type: 'test'
      ...
    # Subtest: stores a bootstrap administrator against their numeric GitHub ID
    ok 2 - stores a bootstrap administrator against their numeric GitHub ID
      ---
      duration_ms: 23.701648
      type: 'test'
      ...
    # Subtest: adds explicit members and writes an audit entry
    ok 3 - adds explicit members and writes an audit entry
      ---
      duration_ms: 8.346352
      type: 'test'
      ...
    # Subtest: keeps environment authority separate from durable role assignments
    ok 4 - keeps environment authority separate from durable role assignments
      ---
      duration_ms: 4.048066
      type: 'test'
      ...
    # Subtest: prevents removing the last durable administrator
    ok 5 - prevents removing the last durable administrator
      ---
      duration_ms: 16.073462
      type: 'test'
      ...
    # Subtest: allows role changes once another administrator exists
    ok 6 - allows role changes once another administrator exists
      ---
      duration_ms: 27.599914
      type: 'test'
      ...
    1..6
ok 27 - instance member service
  ---
  duration_ms: 107.198626
  type: 'suite'
  ...
# Subtest: instance catalog
    # Subtest: returns operational agent and repository fields without installation internals
    ok 1 - returns operational agent and repository fields without installation internals
      ---
      duration_ms: 4.482366
      type: 'test'
      ...
    # Subtest: projects indexing status only for enabled repository and branch entries
    ok 2 - projects indexing status only for enabled repository and branch entries
      ---
      duration_ms: 2.787862
      type: 'test'
      ...
    1..2
ok 28 - instance catalog
  ---
  duration_ms: 7.444565
  type: 'suite'
  ...
# Subtest: instance admin routes
    # Subtest: resolves a GitHub username and creates one durable member assignment
    ok 1 - resolves a GitHub username and creates one durable member assignment
      ---
      duration_ms: 4.750851
      type: 'test'
      ...
    # Subtest: rejects a non-numeric audit limit with a 400
    ok 2 - rejects a non-numeric audit limit with a 400
      ---
      duration_ms: 2.388765
      type: 'test'
      ...
    # Subtest: rejects GitHub usernames with trailing or consecutive hyphens
    ok 3 - rejects GitHub usernames with trailing or consecutive hyphens
      ---
      duration_ms: 4.305405
      type: 'test'
      ...
    1..3
ok 29 - instance admin routes
  ---
  duration_ms: 11.682772
  type: 'suite'
  ...
# [2026-09-03 15:36:40.982 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/home/runner/work/propr/propr/data/propr.test.sqlite"
#     environment: "test"
# [2026-09-03 15:36:41.146 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
# Subtest: direct API execution binds to loopback by default
ok 30 - direct API execution binds to loopback by default
  ---
  duration_ms: 1.080869
  type: 'test'
  ...
# Subtest: containerized API execution remains reachable through its published port
ok 31 - containerized API execution remains reachable through its published port
  ---
  duration_ms: 0.194907
  type: 'test'
  ...
# Subtest: an explicit API listen host overrides environment defaults
ok 32 - an explicit API listen host overrides environment defaults
  ---
  duration_ms: 0.201426
  type: 'test'
  ...
# Subtest: GitHub OAuth authorization stores and sends a random state nonce
ok 33 - GitHub OAuth authorization stores and sends a random state nonce
  ---
  duration_ms: 3.878297
  type: 'test'
  ...
# Subtest: GitHub OAuth callback rejects a mismatched state before token exchange
ok 34 - GitHub OAuth callback rejects a mismatched state before token exchange
  ---
  duration_ms: 1.892325
  type: 'test'
  ...
# [2026-09-03 15:36:42.965 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/home/runner/work/propr/propr/data/propr.test.sqlite"
#     environment: "test"
# [2026-09-03 15:36:42.967 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
# Subtest: returns a standard 429 response after the configured quota
ok 35 - returns a standard 429 response after the configured quota
  ---
  duration_ms: 120.512567
  type: 'test'
  ...
# Subtest: does not charge CORS preflight requests against the quota
ok 36 - does not charge CORS preflight requests against the quota
  ---
  duration_ms: 32.132139
  type: 'test'
  ...
# Subtest: route-level webhook limiting preserves alternate-case raw bodies and rejects excess requests before parsing
ok 37 - route-level webhook limiting preserves alternate-case raw bodies and rejects excess requests before parsing
  ---
  duration_ms: 21.607019
  type: 'test'
  ...
# Subtest: does not let an unconfigured private peer rotate quota buckets with X-Forwarded-For
ok 38 - does not let an unconfigured private peer rotate quota buckets with X-Forwarded-For
  ---
  duration_ms: 16.074434
  type: 'test'
  ...
# Subtest: tunnel trust does not let an unrelated private peer rotate quota buckets
ok 39 - tunnel trust does not let an unrelated private peer rotate quota buckets
  ---
  duration_ms: 19.731445
  type: 'test'
  ...
# Subtest: trusted TLS proxy preserves per-client quotas and secure session cookies
ok 40 - trusted TLS proxy preserves per-client quotas and secure session cookies
  ---
  duration_ms: 36.341638
  type: 'test'
  ...
# Subtest: resolves secure defaults and explicit positive-integer overrides
ok 41 - resolves secure defaults and explicit positive-integer overrides
  ---
  duration_ms: 0.580254
  type: 'test'
  ...
# Subtest: rejects invalid overrides instead of silently disabling protection
ok 42 - rejects invalid overrides instead of silently disabling protection
  ---
  duration_ms: 0.649516
  type: 'test'
  ...
# Subtest: assembled instance permission routes
    # Subtest: captures Express 5 named wildcard parameters as path segments
    ok 1 - captures Express 5 named wildcard parameters as path segments
      ---
      duration_ms: 47.056722
      type: 'test'
      ...
# [2026-09-03 15:36:43.209 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/home/runner/work/propr/propr/data/propr.test.sqlite"
#     environment: "test"
    # Subtest: members can read only the sanitized catalog endpoints
    ok 2 - members can read only the sanitized catalog endpoints
      ---
      duration_ms: 183.630615
      type: 'test'
      ...
    # Subtest: administrators pass every management guard in the route matrix
    ok 3 - administrators pass every management guard in the route matrix
      ---
      duration_ms: 64.411539
      type: 'test'
      ...
    # Subtest: demo users can read only the synthetic Agent Tank usage feed
    ok 4 - demo users can read only the synthetic Agent Tank usage feed
      ---
      duration_ms: 33.232781
      type: 'test'
      ...
    1..4
ok 43 - assembled instance permission routes
  ---
  duration_ms: 330.099398
  type: 'suite'
  ...
# [2026-09-03 15:36:43.536 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status omits disabled configured agents
ok 44 - /api/status omits disabled configured agents
  ---
  duration_ms: 2411.556264
  type: 'test'
  ...
# Subtest: /api/compatibility returns public version contract metadata
ok 45 - /api/compatibility returns public version contract metadata
  ---
  duration_ms: 5.56641
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status returns default Claude fallback when no agents are configured
ok 46 - /api/status returns default Claude fallback when no agents are configured
  ---
  duration_ms: 4.210986
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# [notifications] Failed to project system health snapshot: Error: notification persistence unavailable
#     at projectSystemSnapshot (/home/runner/work/propr/propr/packages/api/test/statusRoutes.test.ts:229:13)
#     at Object.getStatus (/home/runner/work/propr/propr/packages/api/routes/statusRoutes.ts:194:14)
#     at async readStatus (/home/runner/work/propr/propr/packages/api/test/statusRoutes.test.ts:164:3)
#     at async TestContext.<anonymous> (/home/runner/work/propr/propr/packages/api/test/statusRoutes.test.ts:226:16)
#     at async Test.run (node:internal/test_runner/test:1054:7)
#     at async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
# Subtest: /api/status isolates system notification projection failures
ok 47 - /api/status isolates system notification projection failures
  ---
  duration_ms: 10.025151
  type: 'test'
  ...
# Subtest: /api/status surfaces unified agent image outages
ok 48 - /api/status surfaces unified agent image outages
  ---
  duration_ms: 4.384602
  type: 'test'
  ...
# Subtest: /api/status includes warnings field in demo mode
ok 49 - /api/status includes warnings field in demo mode
  ---
  duration_ms: 10.106802
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status caches agent health checks briefly
ok 50 - /api/status caches agent health checks briefly
  ---
  duration_ms: 5.194804
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status marks an unavailable synthetic pool degraded without downgrading direct agents
ok 51 - /api/status marks an unavailable synthetic pool degraded without downgrading direct agents
  ---
  duration_ms: 8.184012
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status reports resolved auth mode and event intake mode
ok 52 - /api/status reports resolved auth mode and event intake mode
  ---
  duration_ms: 10.715097
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status reports connected intake status when routing state is live
ok 53 - /api/status reports connected intake status when routing state is live
  ---
  duration_ms: 4.443468
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status reports disconnected intake status when routing state is down
ok 54 - /api/status reports disconnected intake status when routing state is down
  ---
  duration_ms: 2.975128
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status reports active intake status for polling when the daemon is running
ok 55 - /api/status reports active intake status for polling when the daemon is running
  ---
  duration_ms: 6.006653
  type: 'test'
  ...
# Subtest: /api/status reports disconnected intake status for polling when the daemon is stopped
ok 56 - /api/status reports disconnected intake status for polling when the daemon is stopped
  ---
  duration_ms: 2.238334
  type: 'test'
  ...
# Subtest: /api/status includes routing state published by the daemon
ok 57 - /api/status includes routing state published by the daemon
  ---
  duration_ms: 2.440141
  type: 'test'
  ...
# Subtest: /api/status exposes only validated UI-safe Connect account fields
ok 58 - /api/status exposes only validated UI-safe Connect account fields
  ---
  duration_ms: 5.509927
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status rejects impossible account dates and preserves valid leap-day instants
ok 59 - /api/status rejects impossible account dates and preserves valid leap-day instants
  ---
  duration_ms: 6.971799
  type: 'test'
  ...
# Subtest: /api/status drops malformed or disconnected Connect account state without assuming Community
ok 60 - /api/status drops malformed or disconnected Connect account state without assuming Community
  ---
  duration_ms: 3.141463
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status does not expose Connect account state for a non-Connect intake mode
ok 61 - /api/status does not expose Connect account state for a non-Connect intake mode
  ---
  duration_ms: 1.436704
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Subtest: /api/status reports connected githubAuth for relay-auth deployments
ok 62 - /api/status reports connected githubAuth for relay-auth deployments
  ---
  duration_ms: 14.67889
  type: 'test'
  ...
# Subtest: /api/status reports unknown auth mode and disconnected health when the resolver is bypassed
ok 63 - /api/status reports unknown auth mode and disconnected health when the resolver is bypassed
  ---
  duration_ms: 2.124706
  type: 'test'
  ...
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:30:19)
#     at async getConfigWithClient (/home/runner/work/propr/propr/packages/core/src/config/configStore.ts:17:24)
#     at async loadSummarizationRuntimeState (/home/runner/work/propr/propr/packages/core/src/config/configManagerSummarization.ts:101:19) {
#   code: 'SQLITE_ERROR'
# }
# Error loading summarization warnings: SqliteError: select * from `system_configs` where `key` = 'summarization_runtime_state' limit 1 - no such table: system_configs
#     at Database.prepare (/home/runner/work/propr/propr/node_modules/better-sqlite3/lib/methods/wrappers.js:5:21)
#     at Client_BetterSQLite3._query (/home/runner/work/propr/propr/node_modules/knex/lib/dialects/better-sqlite3/index.js:35:34)
#     at executeQuery (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/query-executioner.js:37:17)
#     at Client_BetterSQLite3.query (/home/runner/work/propr/propr/node_modules/knex/lib/client.js:154:12)
#     at Runner.query (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:141:36)
#     at ensureConnectionCallback (/home/runner/work/propr/propr/node_modules/knex/lib/execution/internal/ensure-connection-callback.js:13:17)
#     at Runner.ensureConnection (/home/runner/work/propr/propr/node_modules/knex/lib/execution/runner.js:318:20)
#     at async Runner.run (/home/runner/work/propr/propr/node_modules/kn

...(truncated)

View Workflow

@propr-dev

propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Applied the requested follow-up changes in commit 8d50d3f

Summary of Changes

Fixed the failed CI check in test/agentImagePreparation.test.ts by using Node’s correct namedExports module-mocking option.

Validation:

  • Fast unit suite: 284 passed, 0 failed
  • Root TypeScript check passed
  • git diff --check passed
  • No visual preview generated because this test-only change is not user-visible
  • No commit created, as requested

🤖 Implementation Details

  • Model: GPT-5.6 Sol (high)
  • Requested By: @github-actions[bot]
  • Time: 4m 41s
  • Tokens: 1,743,665 (1,739,062 in / 4,603 out)
  • Cost: $1.45

Undo ChangesView 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 Implement fixes for issues found by /review /fix or /fix address the null check issue
/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: 5528151996✓

propr-dev[bot] and others added 9 commits September 3, 2026 15:42
…st.ts](/tmp/git-processor/worktrees/integry/propr/pr-2102-followup-2026-09-03T15-37-17/test/agentImagePreparation.test.ts:12) by using Node’s correct `namedExports` module-mocking option.

Fixed the failed CI check in [test/agentImagePreparation.test.ts](/tmp/git-processor/worktrees/integry/propr/pr-2102-followup-2026-09-03T15-37-17/test/agentImagePreparation.test.ts:12) by using Node’s correct `namedExports` module-mocking option.

Validation:

- Fast unit suite: 284 passed, 0 failed
- Root TypeScript check passed
- `git diff --check` passed
- No visual preview generated because this test-only change is not user-visible
- No commit created, as requested

PR: #2102
Comment by: @github-actions[bot] (ID: 5528151996)
Model: gpt-5.6-sol
@integry

integry commented Sep 3, 2026

Copy link
Copy Markdown
Owner

/review opus

@propr-dev

propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

AI Code Review Complete requested by @integry

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — opus

Overall Evaluation

This PR adds an end-to-end visual-preview capability: per-repository policy (UI/CLI/API/config), an agent prompt contract with a .propr/previews/manifest.json, worker-side evidence staging, GitHub attachment publication for both new PRs and follow-up comments, an encrypted upload credential with OAuth refresh coordination, a Settings surface for PAT entry, and a reworked agent-image preparation lifecycle. The feature work is well factored and unusually well tested for its size, and the previously raised "don't commit the preview files" feedback is genuinely resolved.

The problem is not the feature; it is the collateral change to the shared browser-session authentication path. Converting the proactive GitHub token refresh from fire-and-forget to an awaited, fatal check makes ensureAuthenticated return 401 GITHUB_REAUTH_REQUIRED for any session that has no refresh token or is not the owner of the stored upload credential — which is the normal case for classic OAuth App logins and for every non-admin member. That is a lockout-class regression on an unrelated code path and must be fixed before merge. The offline_access scope added alongside it is not a GitHub OAuth scope and cannot supply the refresh tokens the new logic assumes.

Preview media is genuinely kept out of GitprepareVisualPreviewEvidence copies accepted assets to a worker-owned temp directory and then scrubVisualPreviewDirectory restores .propr/previews to HEAD before commitChanges runs, with commitOperations.ts unstaging the directory as a second barrier; packages/core/test/visualPreviewService.test.ts asserts both the tracked-file restore and the ignored-directory case.

Uploads are verified rather than assumedassertUploadedBodyHasNoLocalPaths and assertUploadedBodyContainsUrls in src/github/visualPreviewAttachments.ts fail the publication when GitHub leaves a local path or drops an attachment URL, and the follow-up path uploads before touching the existing comment so a failed upload cannot mutate the work comment (covered in test/visualPreviewAttachments.test.ts).

Credential storage is defensive — AES-256-GCM envelope encryption, explicit rejection of ghu_/ghs_ tokens before persistence, a SQLite refresh lease that serializes cross-process refreshes, and status/route tests that assert token material never appears in responses.

Merge blockers

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

F2: 🔴 Proactive refresh now 401s sessions that have no refresh token

  • Required behavior: Changed authentication behavior must not reject sessions that were previously valid; the PR's goal is to synchronize the shared upload grant, not to invalidate browser sessions.
  • Evidence: packages/api/auth.ts:443-456 — the non-expired branch changed from refreshGitHubTokenIfNeeded(req).catch(...) (result discarded) to const refreshResult = await refreshGitHubTokenWithResult(req); followed by if (refreshResult.status === 'reauth-required' ...) res.status(401).json({ code: 'GITHUB_REAUTH_REQUIRED' }). In packages/api/authGithubTokens.ts, performGitHubTokenRefresh still returns { status: 'reauth-required' } unconditionally when !user.refreshToken, and the newly added shared-grant block returns null (falls through to that check) whenever refreshAndGetForOwner finds no row, a different github_user_id, or source === 'static_token'. Classic OAuth App logins (GH_OAUTH_CLIENT_ID/GH_OAUTH_CLIENT_SECRET, passport-github2) issue non-expiring tokens with no refresh token, so every non-admin member — and any admin whose credential was not captured because captureFromLogin returned false for an already-active different owner, or who stored a PAT under another account — is rejected on every authenticated API request and cannot recover by logging in again.
  • Minimum fix: On the proactive (non-expired) branch, treat only an explicit shared-grant reauth_required / req.user.githubAuthInvalid as fatal, and treat a missing session refresh token as not-needed (continue with the unexpired session token) instead of returning 401.

F3: 🔴 offline_access added to the GitHub authorize scopes

  • Required behavior: The login request must remain a valid GitHub OAuth authorization request; a scope change on the primary browser-login path must not risk rejecting all logins.
  • Evidence: packages/api/auth.ts:288passport.authenticate('github', { scope: ['user:email', 'read:org', 'repo', 'offline_access'] }). offline_access is not a GitHub OAuth scope (GitHub's scope list contains no such value); refresh tokens are produced by enabling expiring user-to-server tokens on a GitHub App, never by requesting a scope on a classic OAuth App. At best the value is inert; at worst GitHub rejects the authorization request as an invalid/unknown scope and no user can log in. Nothing in the diff tests or exercises the authorize URL, and F1's new fatal behavior depends on the refresh tokens this scope cannot deliver.
  • Minimum fix: Remove offline_access from the scope array and rely on the GitHub App / relay configuration for refresh-token issuance.

Suggestions

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

S1: 🟢 Per-request credential lookup on the hot auth path

With the awaited refresh, every authenticated request whose session token starts with gho_/ghp_/github_pat_ now performs visualPreviewOAuthCredentialService.refreshAndGetForOwner(...), which is a SQLite read plus an AES-GCM decrypt (packages/api/authGithubTokens.ts). For a UI that polls status and task lists, this adds a blocking database round-trip to every call for a value that changes at most every few hours. A short in-process cache keyed by credential updated_at, or synchronizing only when the recorded expiry is near, would keep the safety property without the per-request cost. Optional because it is a performance concern rather than a correctness one.

S2: 🟢 Worker startup now hard-fails when the agent image is not ready

src/worker.ts throws when getOperationalStatus().unifiedAgentImage.status !== 'ready' at startup, replacing the previous log-and-continue degraded mode. This is consistent with the objective (do not advertise capacity while an image is building), but it converts a transient registry/network problem into a container crash loop that also takes down queue processing and PR-comment handling, and AgentRegistry still carries a retry timer and a degraded status surface that this path can no longer reach. Consider retrying with backoff before exiting, or gating only task-claiming on image readiness.

S3: 🟢 Preview preparation can abort publication of valid work

In src/jobs/prCommentPostExecution.ts and src/jobs/issueJobPostProcessing.ts, prepareVisualPreviewEvidence now runs before commitAndPush/commitChanges and outside any error isolation, and it runs even when previews are disabled for the repository (only collectVisualPreviewEvidence short-circuits; the git status / git ls-files / git restore / rm -rf scrub always executes). A failure in any of those git or filesystem operations aborts the whole job, so the agent's committed work is never published for a purely optional evidence step. Skipping preparation entirely when settings.enabled === false, and treating preparation failure as "no evidence" rather than a thrown error, would keep the primary workflow independent of the feature.

S4: 🟢 Narrowed auth rate-limit prefix

app.use('/api/auth', createAuthRequestRateLimiter()) became app.use('/api/auth/github', ...). The motivation (session/metadata calls exhausting the 30-per-15-minutes OAuth bucket) is sound and tested, but any other unauthenticated auth-initiation route that does not live under /api/auth/github — for example a Connect start/exchange endpoint — silently drops from the tight bucket to the general 600/minute limiter. Worth confirming the full route list under /api/auth and, if needed, registering the limiter on each initiation/callback path explicitly rather than on a single prefix.

S5: 🟢 Inconsistent handling of an environment-managed credential

usePersonalAccessToken returns 409 VISUAL_PREVIEW_TOKEN_ENVIRONMENT_MANAGED when GITHUB_VISUAL_PREVIEW_TOKEN is set, but useCurrentLogin performs service.replace(...) unconditionally (packages/api/routes/visualPreviewAuthRoutes.ts). Since resolveUploadToken always prefers the environment value, an administrator who clicks "Use my GitHub login" on an environment-managed instance stores a credential that will never be used and sees a status that still reports environment. Mirroring the 409 guard in useCurrentLogin would make the two admin actions behave consistently.

S6: 🟢 Implicit capture of an administrator's browser token

captureVisualPreviewCredentialFromAdminLogin persists the first whitelisted administrator's full repo-scoped session token as the background upload credential at login time, with no prompt (packages/api/services/visualPreviewOAuth.ts). The encryption key falls back to SESSION_SECRET, which .env.example ships as a placeholder. Given the explicit "Use my GitHub login" button already exists in Settings, requiring that explicit action (and requiring a non-placeholder PROPR_CREDENTIAL_ENCRYPTION_KEY/SYSTEM_TASK_SECRET) would reduce the blast radius of storing a personal token at rest.

S7: 🟢 TARGETARCH fallback in the app Dockerfiles

Dockerfile, Dockerfile.node, and docker/Dockerfile.app.prod use gh_arch="${TARGETARCH:-amd64}". TARGETARCH is only populated by BuildKit; with the legacy builder on an arm64 host the build silently selects the amd64 archive and fails at the gh --version check with a confusing error. Detecting the architecture from uname -m when TARGETARCH is empty would make the fallback correct rather than merely defaulted.

Score

The visual-preview feature itself is carefully implemented and well tested, but the PR ships a session-authentication change that will return 401 GITHUB_REAUTH_REQUIRED to ordinary users of a standard OAuth App configuration, together with an unsupported OAuth scope on the same critical path.

Score: 4/10


🤖 Review Details

  • Model: Claude Opus 5
  • Time: 4m 52s
  • Tokens: 207,274 (183,862 in / 23,412 out)
  • Cost: $1.73

View Task


💡 Next step: Comment /fix to address F# merge blockers only.
F# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. Suggestions require a separate ordinary follow-up request.

🤖 Review by ProPR

@integry

integry commented Sep 3, 2026

Copy link
Copy Markdown
Owner

/ultrafix

@propr-dev propr-dev Bot added the ultrafix label Sep 3, 2026
@propr-dev

propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

🔄 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 Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Applied the requested follow-up changes in commit ea8bbf5

Addressed 2 AI review comments (IDs: 5524746253, 5533388381)

Summary of Changes

Implemented selected findings F1–F3 only.

  • F1: Already fixed on this branch; empty sections only remove the placeholder without normalizing body whitespace.
  • F2: Unexpired sessions without refresh tokens now continue normally; explicit invalidation remains fatal.
  • F3: Removed invalid offline_access GitHub scope.

Validation passed:

  • 16 focused tests
  • Core and API typechecks
  • Core and API lint
  • git diff --check

No visual previews were generated because these changes have no visual UI impact. No commit was created.


🤖 Implementation Details

  • Model: GPT-5.6 Sol (high)
  • Requested By: @integry
  • Time: 9m 57s
  • Tokens: 489,990 (485,418 in / 4,572 out)
  • Cost: $0.54

Undo ChangesView 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 Implement fixes for issues found by /review /fix or /fix address the null check issue
/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: 5533427617✓

Implemented selected findings F1–F3 only.

- F1: Already fixed on this branch; empty sections only remove the placeholder without normalizing body whitespace.
- F2: Unexpired sessions without refresh tokens now continue normally; explicit invalidation remains fatal.
- F3: Removed invalid `offline_access` GitHub scope.

Validation passed:

- 16 focused tests
- Core and API typechecks
- Core and API lint
- `git diff --check`

No visual previews were generated because these changes have no visual UI impact. No commit was created.

PR: #2102
Comment by: @integry (ID: 5533427617)
Model: gpt-5.6-sol
@propr-dev

propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

AI Code Review Complete requested by @propr-ultrafix

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex:gpt-5.6-sol

Overall Evaluation

This PR adds repository-scoped visual-preview policies, ephemeral evidence handling, GitHub attachment publication, dedicated encrypted upload credentials, and administrator/CLI configuration. The visible implementation is thoughtfully integrated and well tested, and all authoritative active checks pass. However, two localized correctness issues prevent merge; the review is also partial because the supplied diff explicitly omits ranges.

Ephemeral preview lifecycle — Both initial and follow-up paths prepare preview evidence before committing, exclude .propr/previews from commits, and clean temporary evidence in finally blocks.

Credential separation — Preview upload credentials are managed through dedicated API/UI paths while installation-token Octokit operations remain separate, with targeted tests covering validation and token non-disclosure.

Overall, the PR needs minor but required changes before merge.

Merge blockers

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

F4: 🔴 Branch deletion can disable previews

  • Required behavior: Visual-preview policy is defined as repository-level and branch-independent, so deleting or saving one branch entry must not silently replace the repository’s enabled policy with an unchanged disabled sibling value.
  • Evidence: packages/api/routes/configRepoValidation.ts:88 — preserveRepoVisualPreview records the first explicit incoming branch value and otherwise looks up a single previous branch entry; if the branch carrying the enabled value is removed, an unchanged disabled sibling becomes the repository-wide value.
  • Minimum fix: When no branch entry contains an intentional preview-policy change, preserve the previously resolved repository-wide setting using the same enabled-wins semantics as resolveRepositoryVisualPreviewSettings; use explicit input as the fallback only for a genuinely new repository.

F5: 🔴 Proactive refresh can block requests

  • Required behavior: Proactive credential refresh must fail in a bounded way so an authenticated request with a still-valid token can continue when GitHub or the Connect relay is temporarily unavailable.
  • Evidence: packages/api/auth.ts:443 — normal authenticated requests now await refreshGitHubTokenWithResult; packages/api/authGithubTokens.ts:185 — the awaited GitHub/relay fetch has no abort signal or timeout, so a stalled upstream prevents the documented temporary-failure continuation path from running.
  • Minimum fix: Add a bounded timeout to the refresh operation and classify timeout/abort as temporarily-unavailable while the current access token remains unexpired.

Suggestions

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

No suggestions.

Score

The design and targeted coverage are strong, and current active checks pass, but the repository-policy data-loss case and unbounded authenticated-request stall are merge blockers in changed behavior.

Score: 6/10


🤖 Review Details

  • Model: GPT-5.6 Sol
  • Time: 2m 47s
  • Tokens: 80,918 (72,034 in / 8,884 out)
  • Cost: $0.60
  • Review scope: Partial — PR diff files or ranges were unavailable from GitHub or omitted by the configured review context limit.

View Task


💡 Next step: Comment /fix to address F# merge blockers only.
F# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. Suggestions require a separate ordinary follow-up request.

🤖 Review by ProPR

@propr-dev

propr-dev Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Applied the requested follow-up changes in commit a279a3c

Addressed 1 AI review comment (IDs: 5533613355)

Summary of Changes

Implemented only F4 and F5.

  • F4: Preserves the prior repository-wide visual-preview policy using enabled-wins semantics when branch entries are deleted or unchanged.
  • F5: Adds a 20-second timeout to GitHub/Connect token refresh requests; aborts remain temporarily-unavailable.

Validation passed:

  • Focused config and authentication tests
  • Branch-deletion regression check
  • Timeout/abort regression check
  • API typecheck and lint
  • git diff --check

Only the two cited implementation files changed. No visual previews were created because the changes are backend-only.


🤖 Implementation Details

  • Model: GPT-5.6 Sol (high)
  • Requested By: @propr-ultrafix
  • Time: 12m 43s
  • Tokens: 817,340 (808,851 in / 8,489 out)
  • Cost: $0.89

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 Implement fixes for issues found by /review /fix or /fix address the null check issue
/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

Implemented only F4 and F5.

- F4: Preserves the prior repository-wide visual-preview policy using enabled-wins semantics when branch entries are deleted or unchanged.
- F5: Adds a 20-second timeout to GitHub/Connect token refresh requests; aborts remain `temporarily-unavailable`.

Validation passed:

- Focused config and authentication tests
- Branch-deletion regression check
- Timeout/abort regression check
- API typecheck and lint
- `git diff --check`

Only the two cited implementation files changed. No visual previews were created because the changes are backend-only.

PR: #2102
Comment by: @propr-ultrafix (ID: 0)
Model: gpt-5.6-sol
@propr-dev

propr-dev Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

AI Code Review Complete requested by @propr-ultrafix

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex:gpt-5.6-sol

Overall Evaluation

The PR adds repository-level visual-preview policy, secure upload credentials, preview staging/publication, CLI setup integration, and administrator UI controls. The visible implementation is cohesive and well tested, with no verified merge blocker. However, the supplied diff explicitly omits part of the central attachment implementation, so the PR cannot receive final merge approval from this partial review alone.

Credential isolation — Upload credentials are encrypted, excluded from API responses, reject incompatible GitHub App tokens, and keep static PATs separate from browser sessions.

Safe preview lifecycle — Generated media is staged outside the repository, restored before committing, and published through both initial-PR and preview-only follow-up paths.

Backward-compatible policy handling — Legacy repository settings receive safe defaults, branch entries are synchronized, and API, CLI, and UI inputs are normalized and validated.

Merge blockers

No merge blockers.

Suggestions

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

S1: 🟢 Restore mocked global fetch

The new Connect-refresh test in packages/api/test/authGithubTokens.test.ts assigns globalThis.fetch but does not restore it. Using t.mock.method or restoring the original function in finally would prevent the test from influencing later tests in the same process. This is optional because the current checks pass and it does not affect production behavior.

Score

The reviewed portion is strong and all authoritative head checks pass, but omitted uploader and credential-service ranges create a material verification gap that prevents a complete merge-readiness determination.

Score: 7/10


🤖 Review Details

  • Model: GPT-5.6 Sol
  • Time: 3m 14s
  • Tokens: 82,438 (72,021 in / 10,417 out)
  • Cost: $0.62
  • Review scope: Partial — PR diff files or ranges were unavailable from GitHub or omitted by the configured review context limit.

View Task


💡 Next step: Comment /fix to address F# merge blockers only.
F# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. Suggestions require a separate ordinary follow-up request.

🤖 Review by ProPR

@propr-dev

propr-dev Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ Ultrafix stopped before reaching its goal. Requested goal: 8/10. Last score: 7. The latest review had partial diff coverage, so it cannot establish merge readiness. Manual review and merge are now required.

@integry
integry marked this pull request as ready for review September 4, 2026 07:52
@integry
integry merged commit c895c18 into main Sep 4, 2026
15 of 16 checks passed
@integry
integry deleted the visual-previews-b1b77b35 branch September 4, 2026 07:53
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Full Test Suite Results

Validation failed during: Full test suite.

  • Dependency install: success
  • Workspace build: success
  • Docs validation: success
  • Redis startup: success
  • Full test suite: failure
View Test Output

> propr@0.8.15 test:full:prepared
> npm run test:server


> propr@0.8.15 test:server
> node scripts/run-test-suite.mjs


[1/327] packages/api/test/agentLoginRoutes.test.ts
TAP version 13
# Subtest: agent login session manager
    # Subtest: builds an allowlisted Docker login container without forwarding host secrets
    ok 1 - builds an allowlisted Docker login container without forwarding host secrets
      ---
      duration_ms: 3.061896
      type: 'test'
      ...
    # Subtest: maps a ProPR-managed account to the managed host root and marks its container ownership as safe to normalize
    ok 2 - maps a ProPR-managed account to the managed host root and marks its container ownership as safe to normalize
      ---
      duration_ms: 1.387965
      type: 'test'
      ...
    # Subtest: creates a ProPR-managed credential directory before starting Docker
    ok 3 - creates a ProPR-managed credential directory before starting Docker
      ---
      duration_ms: 2.2729
      type: 'test'
      ...
    # Subtest: fails clearly instead of resolving a default home path inside a containerized API
    ok 4 - fails clearly instead of resolving a default home path inside a containerized API
      ---
      duration_ms: 0.665023
      type: 'test'
      ...
    # Subtest: rejects unsafe credential roots and option-like image names
    ok 5 - rejects unsafe credential roots and option-like image names
      ---
      duration_ms: 0.473365
      type: 'test'
      ...
    # Subtest: streams sanitized output, accepts input, and records successful completion
    ok 6 - streams sanitized output, accepts input, and records successful completion
      ---
      duration_ms: 1.778155
      type: 'test'
      ...
    # Subtest: prevents concurrent logins that write the same credential directory
    ok 7 - prevents concurrent logins that write the same credential directory
      ---
      duration_ms: 0.683688
      type: 'test'
      ...
    # Subtest: pulls a missing agent image before creating the login container
    ok 8 - pulls a missing agent image before creating the login container
      ---
      duration_ms: 0.704407
      type: 'test'
      ...
    # Subtest: renews the session deadline when the user sends input
    ok 9 - renews the session deadline when the user sends input
      ---
      duration_ms: 0.634236
      type: 'test'
      ...
    # Subtest: sweeps only orphaned login containers from the current stack scope
    ok 10 - sweeps only orphaned login containers from the current stack scope
      ---
      duration_ms: 0.572891
      type: 'test'
      ...
    1..10
ok 1 - agent login session manager
  ---
  duration_ms: 13.614678
  type: 'suite'
  ...
# Subtest: agent login routes
    # Subtest: starts and returns only the requesting user login session
    ok 1 - starts and returns only the requesting user login session
      ---
      duration_ms: 0.862493
      type: 'test'
      ...
    # Subtest: accepts the same agent alias on start and follow-up calls, including while disabled
    ok 2 - accepts the same agent alias on start and follow-up calls, including while disabled
      ---
      duration_ms: 0.630369
      type: 'test'
      ...
    # Subtest: rejects interactive login for an unsupported agent type
    ok 3 - rejects interactive login for an unsupported agent type
      ---
      duration_ms: 0.491068
      type: 'test'
      ...
    1..3
ok 2 - agent login routes
  ---
  duration_ms: 2.130063
  type: 'suite'
  ...
# [2026-09-04 08:05:33.971 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/001-agentLoginRoutes.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-09-04 08:05:33.983 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
# [2026-09-04 08:05:33.983 +0000] �[32mINFO�[39m: �[36mShutting down queue...�[39m
# [2026-09-04 08:05:33.983 +0000] �[32mINFO�[39m: �[36mQueue was never initialized, nothing to shutdown�[39m
1..2
# tests 13
# suites 2
# pass 13
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2391.577437

[2/327] packages/api/test/agentRuntimeRoutes.test.ts
TAP version 13
# Subtest: agent runtime package routes
    # Subtest: queues one validated package profile for the unified agent image
    ok 1 - queues one validated package profile for the unified agent image
      ---
      duration_ms: 4.36912
      type: 'test'
      ...
    # Subtest: persists a failed state when queue submission fails
    ok 2 - persists a failed state when queue submission fails
      ---
      duration_ms: 0.890595
      type: 'test'
      ...
    # Subtest: rejects a package missing from an effective runtime before queueing
    ok 3 - rejects a package missing from an effective runtime before queueing
      ---
      duration_ms: 0.803623
      type: 'test'
      ...
    # Subtest: times out slow runtime package validation before queueing
    ok 4 - times out slow runtime package validation before queueing
      ---
      duration_ms: 8.147575
      type: 'test'
      ...
    # Subtest: returns package suggestions from configured runtimes
    ok 5 - returns package suggestions from configured runtimes
      ---
      duration_ms: 1.033112
      type: 'test'
      ...
    # Subtest: enforces PROPR_ADMIN_USERS when configured
    ok 6 - enforces PROPR_ADMIN_USERS when configured
      ---
      duration_ms: 0.408564
      type: 'test'
      ...
    # Subtest: denies runtime package changes by default when no admin policy is configured
    ok 7 - denies runtime package changes by default when no admin policy is configured
      ---
      duration_ms: 0.259665
      type: 'test'
      ...
    # Subtest: requires runtime-management permission before returning runtime package state
    ok 8 - requires runtime-management permission before returning runtime package state
      ---
      duration_ms: 0.372857
      type: 'test'
      ...
    # Subtest: warms the package catalog when an admin loads runtime package state
    ok 9 - warms the package catalog when an admin loads runtime package state
      ---
      duration_ms: 0.472593
      type: 'test'
      ...
    # Subtest: does not warm the package catalog when a member is rejected
    ok 10 - does not warm the package catalog when a member is rejected
      ---
      duration_ms: 4.431667
      type: 'test'
      ...
    # Subtest: resolves the runtime build queue lazily when queueing
    ok 11 - resolves the runtime build queue lazily when queueing
      ---
      duration_ms: 0.993989
      type: 'test'
      ...
    # Subtest: reports apply load failures through the route response
    ok 12 - reports apply load failures through the route response
      ---
      duration_ms: 0.391782
      type: 'test'
      ...
    # Subtest: verifies the current effective images through the runtime-management boundary
    ok 13 - verifies the current effective images through the runtime-management boundary
      ---
      duration_ms: 0.585184
      type: 'test'
      ...
    # Subtest: rejects runtime verification without runtime-management permission
    ok 14 - rejects runtime verification without runtime-management permission
      ---
      duration_ms: 0.269925
      type: 'test'
      ...
    1..14
ok 1 - agent runtime package routes
  ---
  duration_ms: 25.168453
  type: 'suite'
  ...
# [2026-09-04 08:05:36.111 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/002-agentRuntimeRoutes.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-09-04 08:05:36.133 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
1..1
# tests 14
# suites 1
# pass 14
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2200.03425

[3/327] packages/api/test/attachmentUploadCleanup.test.ts
TAP version 13
# Subtest: upload handler removes Multer files rejected before attachment processing
ok 1 - upload handler removes Multer files rejected before attachment processing
  ---
  duration_ms: 8.450682
  type: 'test'
  ...
# Subtest: attachment processing removes temporary and final files when persistence fails
ok 2 - attachment processing removes temporary and final files when persistence fails
  ---
  duration_ms: 5.886828
  type: 'test'
  ...
# Subtest: temporary cleanup refuses paths outside its configured root
ok 3 - temporary cleanup refuses paths outside its configured root
  ---
  duration_ms: 0.712192
  type: 'test'
  ...
# Subtest: temporary cleanup rejects dot-segment paths that resolve above its root
ok 4 - temporary cleanup rejects dot-segment paths that resolve above its root
  ---
  duration_ms: 0.51845
  type: 'test'
  ...
# Subtest: attachment processing rejects path-like draft IDs and still cleans its temp file
ok 5 - attachment processing rejects path-like draft IDs and still cleans its temp file
  ---
  duration_ms: 2.691754
  type: 'test'
  ...
# [2026-09-04 08:05:38.442 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/003-attachmentUploadCleanup.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-09-04 08:05:38.460 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
1..5
# tests 5
# suites 0
# pass 5
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2207.673145

[4/327] packages/api/test/authGithubTokens.test.ts
TAP version 13
# Refreshing GitHub token for user octocat (force=true)
# Successfully refreshed GitHub token for user octocat
# Refreshing GitHub token for user octocat (force=true)
# GitHub token refresh failed with status 503
# Refreshing GitHub token for user octocat (force=true)
# Successfully refreshed GitHub token for user octocat
# Refreshing GitHub token for user octocat (force=true)
# Subtest: isGitHubTokenExpired handles missing, future, exact, and past expiry values
ok 1 - isGitHubTokenExpired handles missing, future, exact, and past expiry values
  ---
  duration_ms: 2.548155
  type: 'test'
  ...
# Subtest: ensureAuthenticated refreshes an expired GitHub token before continuing
ok 2 - ensureAuthenticated refreshes an expired GitHub token before continuing
  ---
  duration_ms: 6.298658
  type: 'test'
  ...
# Subtest: ensureAuthenticated reports a temporary error when refresh fails recoverably
ok 3 - ensureAuthenticated reports a temporary error when refresh fails recoverably
  ---
  duration_ms: 1.75981
  type: 'test'
  ...
# Subtest: refreshes a Connect-issued session through the relay
ok 4 - refreshes a Connect-issued session through the relay
  ---
  duration_ms: 2.581067
  type: 'test'
  ...
# Successfully refreshed GitHub token for user octocat
# Saved refreshed GitHub token for concurrent request by user octocat
# Refreshing GitHub token for user octocat (force=true)
# GitHub token refresh response missing access_token
# Refreshing GitHub token for user octocat (force=true)
# GitHub token refresh error: bad_refresh_token - The refresh token is invalid.
# Marked GitHub OAuth session for user octocat as requiring re-authentication (bad_refresh_token)
# GitHub token expired or revoked, attempting token refresh
# Refreshing GitHub token for user octocat (force=true)
# Successfully refreshed GitHub token for user octocat
# Token refresh successful, client should retry
# Subtest: ensureAuthenticated coalesces concurrent expired-token refreshes for one session
ok 5 - ensureAuthenticated coalesces concurrent expired-token refreshes for one session
  ---
  duration_ms: 11.743459
  type: 'test'
  ...
# Subtest: ensureAuthenticated rejects an expired GitHub token with no refresh token
ok 6 - ensureAuthenticated rejects an expired GitHub token with no refresh token
  ---
  duration_ms: 0.351757
  type: 'test'
  ...
# Subtest: ensureAuthenticated reports a temporary error when refresh omits an access token
ok 7 - ensureAuthenticated reports a temporary error when refresh omits an access token
  ---
  duration_ms: 1.140252
  type: 'test'
  ...
# Subtest: ensureAuthenticated clears the session after an unrecoverable refresh error
ok 8 - ensureAuthenticated clears the session after an unrecoverable refresh error
  ---
  duration_ms: 1.34239
  type: 'test'
  ...
# Subtest: GitHub route auth error emits TOKEN_REFRESHED after a successful refresh
ok 9 - GitHub route auth error emits TOKEN_REFRESHED after a successful refresh
  ---
  duration_ms: 1.147866
  type: 'test'
  ...
# [2026-09-04 08:05:41.282 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/004-authGithubTokens.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-09-04 08:05:41.304 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
1..9
# tests 9
# suites 0
# pass 9
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2741.832789

[5/327] packages/api/test/authRedirect.test.ts
TAP version 13
# Subtest: ordinary auth metadata requests do not consume the OAuth attempt quota
ok 1 - ordinary auth metadata requests do not consume the OAuth attempt quota
  ---
  duration_ms: 68.083069
  type: 'test'
  ...
# Subtest: auth redirect allowlist treats FRONTEND_URL as exact host only
ok 2 - auth redirect allowlist treats FRONTEND_URL as exact host only
  ---
  duration_ms: 13.664417
  type: 'test'
  ...
# Subtest: proxy mode auth redirect fallback returns the hosted UI origin
ok 3 - proxy mode auth redirect fallback returns the hosted UI origin
  ---
  duration_ms: 7.358113
  type: 'test'
  ...
# Subtest: auth redirect allowlist permits subdomains only for explicit wildcard-style hosts
ok 4 - auth redirect allowlist permits subdomains only for explicit wildcard-style hosts
  ---
  duration_ms: 4.271218
  type: 'test'
  ...
# Subtest: auth redirect allowlist permits leading-dot COOKIE_DOMAIN subdomains
ok 5 - auth redirect allowlist permits leading-dot COOKIE_DOMAIN subdomains
  ---
  duration_ms: 4.689571
  type: 'test'
  ...
# Subtest: auth redirect allowlist permits exact additional hosts without permitting their subdomains
ok 6 - auth redirect allowlist permits exact additional hosts without permitting their subdomains
  ---
  duration_ms: 6.674914
  type: 'test'
  ...
# Subtest: auth redirect allowlist permits wildcard entries with protocol prefixes
ok 7 - auth redirect allowlist permits wildcard entries with protocol prefixes
  ---
  duration_ms: 7.087466
  type: 'test'
  ...
# Subtest: auth redirect allowlist rejects invalid URLs and non-http protocols
ok 8 - auth redirect allowlist rejects invalid URLs and non-http protocols
  ---
  duration_ms: 6.826487
  type: 'test'
  ...
# Subtest: auth redirect allowlist only permits cleartext HTTP for localhost
ok 9 - auth redirect allowlist only permits cleartext HTTP for localhost
  ---
  duration_ms: 5.786402
  type: 'test'
  ...
# Subtest: auth redirect allowlist permits configured local IP literals
ok 10 - auth redirect allowlist permits configured local IP literals
  ---
  duration_ms: 4.792584
  type: 'test'
  ...
# Subtest: auth redirect allowlist ignores malformed additional host entries
ok 11 - auth redirect allowlist ignores malformed additional host entries
  ---
  duration_ms: 5.255129
  type: 'test'
  ...
# [2026-09-04 08:05:44.099 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/005-authRedirect.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-09-04 08:05:44.225 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
1..11
# tests 11
# suites 0
# pass 11
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2680.935825

[6/327] packages/api/test/configAgentValidation.antigravity.test.ts
TAP version 13
# Subtest: agent config validation accepts antigravity and rejects gemini for new configs
ok 1 - agent config validation accepts antigravity and rejects gemini for new configs
  ---
  duration_ms: 1674.25804
  type: 'test'
  ...
# [2026-09-04 08:05:46.467 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/006-configAgentValidation.antigravity.test.ts/propr.test.sqlite"
#     environment: "test"
1..1
# tests 1
# suites 0
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2249.878961

[7/327] packages/api/test/configReloadSubscription.test.ts
TAP version 13
# Subtest: API config subscription serializes the startup reload with settings updates
ok 1 - API config subscription serializes the startup reload with settings updates
  ---
  duration_ms: 5.193504
  type: 'test'
  ...
# Subtest: API config subscription reloads repository updates
ok 2 - API config subscription reloads repository updates
  ---
  duration_ms: 0.412442
  type: 'test'
  ...
# Subtest: API config subscription ignores unrelated and malformed events
ok 3 - API config subscription ignores unrelated and malformed events
  ---
  duration_ms: 0.302215
  type: 'test'
  ...
# Subtest: a failed API settings reload does not block the next notification
ok 4 - a failed API settings reload does not block the next notification
  ---
  duration_ms: 0.382155
  type: 'test'
  ...
1..4
# tests 4
# suites 0
# pass 4
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 247.798343

[8/327] packages/api/test/configRepoRoutes.test.ts
TAP version 13
# Subtest: GET repository config returns false for legacy entries with a missing option
ok 1 - GET repository config returns false for legacy entries with a missing option
  ---
  duration_ms: 4.957031
  type: 'test'
  ...
# Subtest: POST repository config persists an enabled option without enabling other repositories
ok 2 - POST repository config persists an enabled option without enabling other repositories
  ---
  duration_ms: 3.745765
  type: 'test'
  ...
# Subtest: POST repository config synchronizes changed visual previews across branch entries
ok 3 - POST repository config synchronizes changed visual previews across branch entries
  ---
  duration_ms: 1.564536
  type: 'test'
  ...
# Subtest: POST repository config preserves an omitted option for existing repositories
ok 4 - POST repository config preserves an omitted option for existing repositories
  ---
  duration_ms: 1.201397
  type: 'test'
  ...
# [2026-09-04 08:05:49.257 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/008-configRepoRoutes.test.ts/propr.test.sqlite"
#     environment: "test"
1..4
# tests 4
# suites 0
# pass 4
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2255.495123

[9/327] packages/api/test/configRepoValidation.test.ts
TAP version 13
# Subtest: repository config defaults missing automatic failed-CI follow-up to false
ok 1 - repository config defaults missing automatic failed-CI follow-up to false
  ---
  duration_ms: 1.749542
  type: 'test'
  ...
# Subtest: repository config accepts visual preview types and trims instructions
ok 2 - repository config accepts visual preview types and trims instructions
  ---
  duration_ms: 0.334365
  type: 'test'
  ...
# Subtest: repository config defaults omitted visual preview types
ok 3 - repository config defaults omitted visual preview types
  ---
  duration_ms: 0.225381
  type: 'test'
  ...
# Subtest: repository config rejects invalid visual preview settings
ok 4 - repository config rejects invalid visual preview settings
  ---
  duration_ms: 0.336981
  type: 'test'
  ...
# Subtest: repository config accepts explicit automatic failed-CI follow-up booleans
ok 5 - repository config accepts explicit automatic failed-CI follow-up booleans
  ---
  duration_ms: 0.21894
  type: 'test'
  ...
# Subtest: repository config rejects non-boolean automatic failed-CI follow-up values
ok 6 - repository config rejects non-boolean automatic failed-CI follow-up values
  ---
  duration_ms: 0.36903
  type: 'test'
  ...
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 251.202479

[10/327] packages/api/test/configRoutesIndexingSummarization.test.ts
TAP version 13
# [2026-09-04 08:05:52.013 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/010-configRoutesIndexingSummarization.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-09-04 08:05:52.034 +0000] �[32mINFO�[39m: �[36mRunning database migrations...�[39m
# Subtest: postSummarizationSettings trims model-specific aliases before saving
ok 1 - postSummarizationSettings trims model-specific aliases before saving
  ---
  duration_ms: 1990.962309
  type: 'test'
  ...
# Subtest: postSummarizationSettings rejects enabled summarization without a primary alias
ok 2 - postSummarizationSettings rejects enabled summarization without a primary alias
  ---
  duration_ms: 6.306383
  type: 'test'
  ...
# [2026-09-04 08:05:52.287 +0000] �[32mINFO�[39m: �[36mDatabase migrations completed successfully�[39m
# [2026-09-04 08:05:52.289 +0000] �[32mINFO�[39m: �[36mSuccessfully loaded agents configuration�[39m
#     agentCount: 2
# [2026-09-04 08:05:52.289 +0000] �[32mINFO�[39m: �[36mSuccessfully loaded summarization settings�[39m
#     summarization: {
#       "enabled": false,
#       "agent_alias": "",
#       "fallback_agent_alias": "",
#       "custom_prompt": ""
#     }
# [2026-09-04 08:05:52.291 +0000] �[32mINFO�[39m: �[36mSuccessfully saved summarization settings�[39m
#     summarization: {
#       "enabled": true,
#       "agent_alias": "codex:gpt-5.5",
#       "fallback_agent_alias": "fallback:gpt-5.4",
#       "custom_prompt": ""
#     }
# [2026-09-04 08:05:52.292 +0000] �[32mINFO�[39m: �[36mSuccessfully loaded summarization settings�[39m
#     summarization: {
#       "enabled": true,
#       "agent_alias": "codex:gpt-5.5",
#       "fallback_agent_alias": "fallback:gpt-5.4",
#       "custom_prompt": ""
#     }
# [2026-09-04 08:05:52.304 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
1..2
# tests 2
# suites 0
# pass 2
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2425.76684

[11/327] packages/api/test/connectAuth.test.ts
TAP version 13
# Subtest: relay tunnel mode uses Connect without local OAuth credentials
ok 1 - relay tunnel mode uses Connect without local OAuth credentials
  ---
  duration_ms: 1.167243
  type: 'test'
  ...
# Subtest: relay tunnel mode preserves Connect for operator-supplied service endpoints
ok 2 - relay tunnel mode preserves Connect for operator-supplied service endpoints
  ---
  duration_ms: 0.206666
  type: 'test'
  ...
# Subtest: local relay mode uses Connect without a per-instance OAuth App
ok 3 - local relay mode uses Connect without a per-instance OAuth App
  ---
  duration_ms: 0.235099
  type: 'test'
  ...
# Subtest: off-tunnel relay inference rejects callbacks outside the exact loopback allowlist
ok 4 - off-tunnel relay inference rejects callbacks outside the exact loopback allowlist
  ---
  duration_ms: 0.314528
  type: 'test'
  ...
# Subtest: off-tunnel custom relay enrollment does not infer hosted Connect auth
ok 5 - off-tunnel custom relay enrollment does not infer hosted Connect auth
  ---
  duration_ms: 0.138689
  type: 'test'
  ...
# Subtest: literal example OAuth placeholders do not enable GitHub web auth
ok 6 - literal example OAuth placeholders do not enable GitHub web auth
  ---
  duration_ms: 0.122339
  type: 'test'
  ...
# Subtest: explicit custom GitHub web auth remains supported
ok 7 - explicit custom GitHub web auth remains supported
  ---
  duration_ms: 0.161031
  type: 'test'
  ...
# Subtest: explicit custom GitHub web auth wins over relay inference off-tunnel
ok 8 - explicit custom GitHub web auth wins over relay inference off-tunnel
  ---
  duration_ms: 0.146774
  type: 'test'
  ...
# Subtest: Connect authorization URL carries the exact callback and CSRF state
ok 9 - Connect authorization URL carries the exact callback and CSRF state
  ---
  duration_ms: 0.479106
  type: 'test'
  ...
# Subtest: redeems a Connect code server-to-server without exposing the relay token in the body
ok 10 - redeems a Connect code server-to-server without exposing the relay token in the body
  ---
  duration_ms: 20.810105
  type: 'test'
  ...
# Subtest: binds the Connect identity username to the validated token owner
ok 11 - binds the Connect identity username to the validated token owner
  ---
  duration_ms: 0.846223
  type: 'test'
  ...
# Subtest: preserves expiring OAuth grant fields returned by Connect
ok 12 - preserves expiring OAuth grant fields returned by Connect
  ---
  duration_ms: 0.714886
  type: 'test'
  ...
1..12
# tests 12
# suites 0
# pass 12
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 434.847576

[12/327] packages/api/test/connectAuthCallback.test.ts
TAP version 13
# [visual-preview] Could not capture OAuth upload credential during login: select `github_user_id`, `role`, `source` from `instance_members` where `github_user_id` = '123' limit 1 - no such table: instance_members
# [2026-09-04 08:05:55.523 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/012-connectAuthCallback.test.ts/propr.test.sqlite"
#     environment: "test"
# Subtest: Connect login preserves a validated redirect_to across Passport session regeneration
ok 1 - Connect login preserves a validated redirect_to across Passport session regeneration
  ---
  duration_ms: 3044.392698
  type: 'test'
  ...
# [2026-09-04 08:05:58.563 +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 5681.461329

[13/327] packages/api/test/corsValidation.test.ts
TAP version 13
# Subtest: CORS allows the hosted UI origin under proxy mode
ok 1 - CORS allows the hosted UI origin under proxy mode
  ---
  duration_ms: 1.065653
  type: 'test'
  ...
# Subtest: CORS rejects unrelated origins under proxy mode
ok 2 - CORS rejects unrelated origins under proxy mode
  ---
  duration_ms: 0.247273
  type: 'test'
  ...
# Subtest: CORS allows requests with no origin
ok 3 - CORS allows requests with no origin
  ---
  duration_ms: 0.144941
  type: 'test'
  ...
# Subtest: CORS allows localhost for development
ok 4 - CORS allows localhost for development
  ---
  duration_ms: 0.14989
  type: 'test'
  ...
# Subtest: CORS rejects non-http(s) localhost schemes
ok 5 - CORS rejects non-http(s) localhost schemes
  ---
  duration_ms: 0.237434
  type: 'test'
  ...
# Subtest: CORS allows COOKIE_DOMAIN subdomains for preview environments
ok 6 - CORS allows COOKIE_DOMAIN subdomains for preview environments
  ---
  duration_ms: 0.166571
  type: 'test'
  ...
# Subtest: CORS preserves http COOKIE_DOMAIN preview compatibility
ok 7 - CORS preserves http COOKIE_DOMAIN preview compatibility
  ---
  duration_ms: 0.187
  type: 'test'
  ...
# Subtest: CORS validator factory throws on an invalid FRONTEND_URL
ok 8 - CORS validator factory throws on an invalid FRONTEND_URL
  ---
  duration_ms: 0.263813
  type: 'test'
  ...
# Subtest: Express CORS boundary is sanitized in development mode
ok 9 - Express CORS boundary is sanitized in development mode
  ---
  duration_ms: 81.888711
  type: 'test'
  ...
# Subtest: Express CORS boundary is sanitized in production mode
ok 10 - Express CORS boundary is sanitized in production mode
  ---
  duration_ms: 27.52445
  type: 'test'
  ...
1..10
# tests 10
# suites 0
# pass 10
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 487.481133

[14/327] packages/api/test/demoMode.test.ts
TAP version 13
# Subtest: demoModeReadOnlyMiddleware rejects mutating requests in demo mode
ok 1 - demoModeReadOnlyMiddleware rejects mutating requests in demo mode
  ---
  duration_ms: 6.328983
  type: 'test'
  ...
# Subtest: demoModeReadOnlyMiddleware blocks auth metadata mutations
ok 2 - demoModeReadOnlyMiddleware blocks auth metadata mutations
  ---
  duration_ms: 4.330597
  type: 'test'
  ...
# Subtest: isDemoMode accepts common truthy environment values
ok 3 - isDemoMode accepts common truthy environment values
  ---
  duration_ms: 1.128981
  type: 'test'
  ...
# Subtest: configured demo mode keeps auth and middleware on the same startup value
ok 4 - configured demo mode keeps auth and middleware on the same startup value
  ---
  duration_ms: 1.716309
  type: 'test'
  ...
# Subtest: demo Redis facade covers read-only route Redis usage
ok 5 - demo Redis facade covers read-only route Redis usage
  ---
  duration_ms: 3.639515
  type: 'test'
  ...
# Subtest: demo Express GET routes work with the in-memory Redis facade
ok 6 - demo Express GET routes work with the in-memory Redis facade
  ---
  duration_ms: 56.701712
  type: 'test'
  ...
# Subtest: ensureAuthenticated attaches the synthetic demo user
ok 7 - ensureAuthenticated attaches the synthetic demo user
  ---
  duration_ms: 1.553364
  type: 'test'
  ...
# Subtest: ensureAuthenticated ignores bearer auth and attaches the synthetic demo user in demo mode
ok 8 - ensureAuthenticated ignores bearer auth and attaches the synthetic demo user in demo mode
  ---
  duration_ms: 1.458658
  type: 'test'
  ...
# Subtest: demo repository metadata resolves enabled configured repositories
ok 9 - demo repository metadata resolves enabled configured repositories
  ---
  duration_ms: 14.078401
  type: 'test'
  ...
# Subtest: repository config branch validation documents ProPR-supported branch names
ok 10 - repository config branch validation documents ProPR-supported branch names
  ---
  duration_ms: 2.557161
  type: 'test'
  ...
# [2026-09-04 08:06:02.061 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/014-demoMode.test.ts/propr.test.sqlite"
#     environment: "test"
# Subtest: /api/github/repos returns configured and persisted repositories in demo mode
ok 11 - /api/github/repos returns configured and persisted repositories in demo mode
  ---
  duration_ms: 258.896747
  type: 'test'
  ...
# [2026-09-04 08:06:02.405 +0000] �[32mINFO�[39m: �[36mSuccessfully saved monitored repositories�[39m
#     repos: [
#       {
#         "id": "1",
#         "name": "integry/propr",
#         "enabled": true
#       },
#       {
#         "id": "2",
#         "name": "integry/private",
#         "enabled": true
#       },
#       {
#         "id": "3",
#         "name": "integry/disabled",
#         "enabled": false
#       }
#     ]
# [2026-09-04 08:06:02.406 +0000] �[32mINFO�[39m: �[36mSuccessfully loaded all monitored repositories�[39m
#     total_repos: 3
# Subtest: demo repository metadata resolves persisted repositories without configured allowlists
ok 12 - demo repository metadata resolves persisted repositories without configured allowlists
  ---
  duration_ms: 20.178446
  type: 'test'
  ...
# Subtest: demo repository metadata ignores malformed database repository names
ok 13 - demo repository metadata ignores malformed database repository names
  ---
  duration_ms: 2.462795
  type: 'test'
  ...
# [2026-09-04 08:06:02.418 +0000] �[32mINFO�[39m: �[36mSuccessfully loaded all monitored repositories�[39m
#     total_repos: 0
# Subtest: planner demo reads use the curated database without owner or repository allowlists
ok 14 - planner demo reads use the curated database without owner or repository allowlists
  ---
  duration_ms: 18.046792
  type: 'test'
  ...
# [2026-09-04 08:06:02.441 +0000] �[32mINFO�[39m: �[36mSuccessfully saved monitored repositories�[39m
#     repos: [
#       {
#         "id": "1",
#         "name": "integry/propr",
#         "enabled": true
#       },
#       {
#         "id": "2",
#         "name": "integry/private",
#         "enabled": true
#       }
#     ]
# Subtest: repo todo demo reads use the curated database without owner filters
ok 15 - repo todo demo reads use the curated database without owner filters
  ---
  duration_ms: 7.747565
  type: 'test'
  ...
# Subtest: auth demo-mode metadata endpoint reports startup environment value
ok 16 - auth demo-mode metadata endpoint reports startup environment value
  ---
  duration_ms: 7.387111
  type: 'test'
  ...
1..16
# tests 16
# suites 0
# pass 16
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2893.853037

[15/327] packages/api/test/dockerCommandSafety.test.ts
TAP version 13
# Subtest: accepts Docker IDs and ProPR-generated container names
ok 1 - accepts Docker IDs and ProPR-generated container names
  ---
  duration_ms: 0.960726
  type: 'test'
  ...
# Subtest: rejects container references that could be parsed as options or shell syntax
ok 2 - rejects container references that could be parsed as options or shell syntax
  ---
  duration_ms: 0.152364
  type: 'test'
  ...
# Subtest: production subprocess call sites do not invoke a command shell
ok 3 - production subprocess call sites do not invoke a command shell
  ---
  duration_ms: 78.269607
  type: 'test'
  ...
# Subtest: no-shell guard rejects aliases and enabled shell options
ok 4 - no-shell guard rejects aliases and enabled shell options
  ---
  duration_ms: 5.048229
  type: 'test'
  ...
1..4
# tests 4
# suites 0
# pass 4
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 923.223691

[16/327] packages/api/test/instanceAuthorization.test.ts
TAP version 13
# Subtest: instance authorization
    # Subtest: defaults unassigned authenticated users to members on a new installation
    ok 1 - defaults unassigned authenticated users to members on a new installation
      ---
      duration_ms: 11.72377
      type: 'test'
      ...
    # Subtest: refuses startup until a bootstrap or durable administrator exists
    ok 2 - refuses startup until a bootstrap or durable administrator exists
      ---
      duration_ms: 4.802979
      type: 'test'
      ...
    # Subtest: grants full admin permissions to PROPR_ADMIN_USERS
    ok 3 - grants full admin permissions to PROPR_ADMIN_USERS
      ---
      duration_ms: 2.807839
      type: 'test'
      ...
    # Subtest: resolves durable roles by numeric GitHub ID after a username change
    ok 4 - resolves durable roles by numeric GitHub ID after a username change
      ---
      duration_ms: 2.774097
      type: 'test'
      ...
    # Subtest: resolves demo users without installation permissions
    ok 5 - resolves demo users without installation permissions
      ---
      duration_ms: 5.342689
      type: 'test'
      ...
    # Subtest: permission middleware rejects members with a stable error code
    ok 6 - permission middleware rejects members with a stable error code
      ---
      duration_ms: 2.2143
      type: 'test'
      ...
    # Subtest: current-user responses expose capabilities without OAuth credentials
    ok 7 - current-user responses expose capabilities without OAuth credentials
      ---
      duration_ms: 2.783484
      type: 'test'
      ...
    1..7
ok 1 - instance authorization
  ---
  duration_ms: 33.807377
  type: 'suite'
  ...
# Subtest: instance member service
    # Subtest: does not let an unassigned user claim the initial administrator role
    ok 1 - does not let an unassigned user claim the initial administrator role
      ---
      duration_ms: 3.132036
      type: 'test'
      ...
    # Subtest: stores a bootstrap administrator against their numeric GitHub ID
    ok 2 - stores a bootstrap administrator against their numeric GitHub ID
      ---
      duration_ms: 5.949974
      type: 'test'
      ...
    # Subtest: adds explicit members and writes an audit entry
    ok 3 - adds explicit members and writes an audit entry
      ---
      duration_ms: 5.004167
      type: 'test'
      ...
    # Subtest: keeps environment authority separate from durable role assignments
    ok 4 - keeps environment authority separate from durable role assignments
      ---
      duration_ms: 2.564706
      type: 'test'
      ...
    # Subtest: prevents removing the last durable administrator
    ok 5 - prevents removing the last durable administrator
      ---
      duration_ms: 4.211334
      type: 'test'
      ...
    # Subtest: allows role changes once another administrator exists
    ok 6 - allows role changes once another administrator exists
      ---
      duration_ms: 5.625558
      type: 'test'
      ...
    1..6
ok 2 - instance member service
  ---
  duration_ms: 26.887811
  type: 'suite'
  ...
# Subtest: instance catalog
    # Subtest: returns operational agent and repository fields without installation internals
    ok 1 - returns operational agent and repository fields without installation internals
      ---
      duration_ms: 2.191898
      type: 'test'
      ...
    # Subtest: projects indexing status only for enabled repository and branch entries
    ok 2 - projects indexing status only for enabled repository and branch entries
      ---
      duration_ms: 1.712622
      type: 'test'
      ...
    1..2
ok 3 - instance catalog
  ---
  duration_ms: 3.996762
  type: 'suite'
  ...
# Subtest: instance admin routes
    # Subtest: resolves a GitHub username and creates one durable member assignment
    ok 1 - resolves a GitHub username and creates one durable member assignment
      ---
      duration_ms: 2.69541
      type: 'test'
      ...
    # Subtest: rejects a non-numeric audit limit with a 400
    ok 2 - rejects a non-numeric audit limit with a 400
      ---
      duration_ms: 1.495436
      type: 'test'
      ...
    # Subtest: rejects GitHub usernames with trailing or consecutive hyphens
    ok 3 - rejects GitHub usernames with trailing or consecutive hyphens
      ---
      duration_ms: 1.530402
      type: 'test'
      ...
    1..3
ok 4 - instance admin routes
  ---
  duration_ms: 5.836321
  type: 'suite'
  ...
# [2026-09-04 08:06:05.651 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/016-instanceAuthorization.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-09-04 08:06:05.716 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
1..4
# tests 18
# suites 4
# pass 18
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2331.179506

[17/327] packages/api/test/listenAddress.test.ts
TAP version 13
# Subtest: direct API execution binds to loopback by default
ok 1 - direct API execution binds to loopback by default
  ---
  duration_ms: 1.118822
  type: 'test'
  ...
# Subtest: containerized API execution remains reachable through its published port
ok 2 - containerized API execution remains reachable through its published port
  ---
  duration_ms: 0.171701
  type: 'test'
  ...
# Subtest: an explicit API listen host overrides environment defaults
ok 3 - an explicit API listen host overrides environment defaults
  ---
  duration_ms: 0.19232
  type: 'test'
  ...
1..3
# tests 3
# suites 0
# pass 3
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 241.029168

[18/327] packages/api/test/liveDetailsAntigravity.test.ts
TAP version 13
# Failed to read GitHub App private key: ENOENT: no such file or directory, open '/tmp/missing-key.pem'
# Ensure GH_PRIVATE_KEY_PATH is set correctly in your .env file and points to a valid private key file.
# Subtest: stored output detection recognizes Antigravity stream JSON
ok 1 - stored output detection recognizes Antigravity stream JSON
  ---
  duration_ms: 2.270105
  type: 'test'
  ...
# Subtest: stored output detection keeps Codex message JSONL classified as Codex
ok 2 - stored output detection keeps Codex message JSONL classified as Codex
  ---
  duration_ms: 1.427509
  type: 'test'
  ...
# Subtest: stored output detection keeps Codex result JSONL classified as Codex
ok 3 - stored output detection keeps Codex result JSONL classified as Codex
  ---
  duration_ms: 0.874555
  type: 'test'
  ...
# Subtest: stored output detection keeps Codex tool result JSONL classified as Codex
ok 4 - stored output detection keeps Codex tool result JSONL classified as Codex
  ---
  duration_ms: 0.200895
  type: 'test'
  ...
# Subtest: stored output detection does not treat generic init JSONL as Antigravity
ok 5 - stored output detection does not treat generic init JSONL as Antigravity
  ---
  duration_ms: 0.328815
  type: 'test'
  ...
# Subtest: stored output detection recognizes Antigravity JSONL from result stats when init is missing
ok 6 - stored output detection recognizes Antigravity JSONL from result stats when init is missing
  ---
  duration_ms: 0.337301
  type: 'test'
  ...
# Subtest: stored output detection keeps generic result token stats classified as Codex
ok 7 - stored output detection keeps generic result token stats classified as Codex
  ---
  duration_ms: 0.195395
  type: 'test'
  ...
# Subtest: stored output detection recognizes truncated Antigravity result JSON
ok 8 - stored output detection recognizes truncated Antigravity result JSON
  ---
  duration_ms: 0.19774
  type: 'test'
  ...
# Subtest: stored output detection recognizes Antigravity message JSON with model metadata
ok 9 - stored output detection recognizes Antigravity message JSON with model metadata
  ---
  duration_ms: 0.367837
  type: 'test'
  ...
# Subtest: stored output parsing renders only Antigravity analysis events through live details
ok 10 - stored output parsing renders only Antigravity analysis events through live details
  ---
  duration_ms: 81.552935
  type: 'test'
  ...
# Subtest: Antigravity 1.1.12 stream text remains visible through live details
ok 11 - Antigravity 1.1.12 stream text remains visible through live details
  ---
  duration_ms: 2.884835
  type: 'test'
  ...
# Subtest: stored output detection and live-details rendering consume Antigravity stream arrays
ok 12 - stored output detection and live-details rendering consume Antigravity stream arrays
  ---
  duration_ms: 1.603669
  type: 'test'
  ...
# Subtest: Antigravity cache-only stream usage remains visible through live details
ok 13 - Antigravity cache-only stream usage remains visible through live details
  ---
  duration_ms: 1.088816
  type: 'test'
  ...
# Subtest: stored output parsing filters Antigravity transcript tool items
ok 14 - stored output parsing filters Antigravity transcript tool items
  ---
  duration_ms: 1.084077
  type: 'test'
  ...
# Subtest: Claude-format Antigravity conversation files render only planner analysis
ok 15 - Claude-format Antigravity conversation files render only planner analysis
  ---
  duration_ms: 1.38505
  type: 'test'
  ...
# Subtest: execution detail fallback filters Antigravity transcript tool rows
ok 16 - execution detail fallback filters Antigravity transcript tool rows
  ---
  duration_ms: 1.057627
  type: 'test'
  ...
# [2026-09-04 08:06:08.483 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/018-liveDetailsAntigravity.test.ts/propr.test.sqlite"
#     environment: "test"
1..16
# tests 16
# suites 0
# pass 16
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2258.362639

[19/327] packages/api/test/liveDetailsCodexDbFallback.test.ts
TAP version 13
# [live-details] jobId: integry-propr-1915-codex, taskId: integry-propr-1915-codex
# [live-details] Trying Redis fallback
# [live-details] stateKey: worker:state:integry-propr-1915-codex, hasData: false
# [live-details] No state data found in Redis
# [live-details] Fetching sessionId from SQLite for taskId: integry-propr-1915-codex
# [live-details] Found sessionId in SQLite: codex-db-session
# [live-details] Using sessionId: codex-db-session
# Subtest: Codex database lifecycle fallback has exact canonical event parity
ok 1 - Codex database lifecycle fallback has exact canonical event parity
  ---
  duration_ms: 4.448488
  type: 'test'
  ...
# Subtest: Codex command lifecycle pairs starts and completions and recovers an unmatched completion
ok 2 - Codex command lifecycle pairs starts and completions and recovers an unmatched completion
  ---
  duration_ms: 0.255127
  type: 'test'
  ...
# Subtest: Codex database fallback retains row content for minimal error metadata
ok 3 - Codex database fallback retains row content for minimal error metadata
  ---
  duration_ms: 0.167072
  type: 'test'
  ...
# Subtest: Codex database fallback retains row content and error flag for minimal tool-result metadata
ok 4 - Codex database fallback retains row content and error flag for minimal tool-result metadata
  ---
  duration_ms: 0.159628
  type: 'test'
  ...
# Subtest: Codex database fallback ignores envelopes while retaining text, errors, and todos
ok 5 - Codex database fallback ignores envelopes while retaining text, errors, and todos
  ---
  duration_ms: 0.454329
  type: 'test'
  ...
# [live-details] Checking Claude conversation path: not found
# [live-details] Claude conversation file not found, trying active Redis output
# [live-details] Claude conversation file not found, trying stored execution output fallback
# [live-details] No stored execution logs found in Redis for session fallback
# [live-details] Stored execution output fallback unavailable, trying database fallback
# [live-details] jobId: integry-propr-1915-codex, taskId: integry-propr-1915-codex
# [live-details] Trying Redis fallback
# [live-details] stateKey: worker:state:integry-propr-1915-codex, hasData: false
# [live-details] No state data found in Redis
# [live-details] Fetching sessionId from SQLite for taskId: integry-propr-1915-codex
# [live-details] Found sessionId in SQLite: codex-db-session
# [live-details] Using sessionId: codex-db-session
# [live-details] Checking Claude conversation path: not found
# [live-details] Claude conversation file not found, trying active Redis output
# [live-details] Claude conversation file not found, trying stored execution output fallback
# [live-details] No stored execution logs found in Redis for session fallback
# [live-details] Stored execution output fallback unavailable, trying database fallback
# Subtest: live-details database fallback preserves token usage and stable event IDs
ok 6 - live-details database fallback preserves token usage and stable event IDs
  ---
  duration_ms: 20.421697
  type: 'test'
  ...
# [2026-09-04 08:06:10.839 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/019-liveDetailsCodexDbFallback.test.ts/propr.test.sqlite"
#     environment: "test"
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2007.918499

[20/327] packages/api/test/liveDetailsOpenCodeParser.test.ts
TAP version 13
# Subtest: detectStoredOutputFormat keeps Codex message streams with session_id as codex
ok 1 - detectStoredOutputFormat keeps Codex message streams with session_id as codex
  ---
  duration_ms: 1658.074672
  type: 'test'
  ...
# Subtest: detectStoredOutputFormat does not classify bare conversation_id JSON as Claude
ok 2 - detectStoredOutputFormat does not classify bare conversation_id JSON as Claude
  ---
  duration_ms: 4.643423
  type: 'test'
  ...
# Subtest: detectStoredOutputFormat keeps Claude-shaped conversation_id JSON as Claude
ok 3 - detectStoredOutputFormat keeps Claude-shaped conversation_id JSON as Claude
  ---
  duration_ms: 1.854809
  type: 'test'
  ...
# Subtest: parseOpenCodeOutputToConversationResult separates structured assistant text parts
ok 4 - parseOpenCodeOutputToConversationResult separates structured assistant text parts
  ---
  duration_ms: 19.538464
  type: 'test'
  ...
# Subtest: detectStoredOutputFormat recognizes whole-document pretty OpenCode JSON
ok 5 - detectStoredOutputFormat recognizes whole-document pretty OpenCode JSON
  ---
  duration_ms: 1.549517
  type: 'test'
  ...
# Subtest: detectStoredOutputFormat recognizes OpenCode assistant parts without session identifiers
ok 6 - detectStoredOutputFormat recognizes OpenCode assistant parts without session identifiers
  ---
  duration_ms: 0.853406
  type: 'test'
  ...
# Subtest: detectStoredOutputFormat recognizes whole-document pretty Claude JSON arrays
ok 7 - detectStoredOutputFormat recognizes whole-document pretty Claude JSON arrays
  ---
  duration_ms: 0.85572
  type: 'test'
  ...
# [2026-09-04 08:06:12.961 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/020-liveDetailsOpenCodeParser.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-09-04 08:06:12.992 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
1..7
# tests 7
# suites 0
# pass 7
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2359.996711

[21/327] packages/api/test/mergedPrTaskCancellation.test.ts
TAP version 13
# [webhook] Merged PR acme/widgets\#42: cancelled 3/3 active task(s)
# [webhook] Failed to cancel task for merged PR {
#   taskId: 'task-a',
#   repository: 'acme/widgets',
#   prNumber: 42,
#   error: Error: container stop blew up
#       at Object.stopTask (/home/runner/work/propr/propr/packages/api/test/mergedPrTaskCancellation.test.ts:89:34)
#       at stopNext (/home/runner/work/propr/propr/packages/api/webhookHandler.ts:210:35)
#       at <anonymous> (/home/runner/work/propr/propr/packages/api/webhookHandler.ts:236:104)
#       at Function.from (<anonymous>)
#       at cancelActiveTasksForMergedPR (/home/runner/work/propr/propr/packages/api/webhookHandler.ts:236:27)
#       at async TestContext.<anonymous> (/home/runner/work/propr/propr/packages/api/test/mergedPrTaskCancellation.test.ts:94:19)
#       at async Test.run (node:internal/test_runner/test:1054:7)
#       at async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
# }
# [webhook] Task task-a for merged PR acme/widgets\#42 was stopped but the cancellation was not durably recorded; the abort signal remains for the worker to observe
# [webhook] Merged PR acme/widgets\#42: cancelled 2/3 active task(s), 1 failed
# [webhook] Task task-a for merged PR acme/widgets\#42 was no longer active
# [webhook] Merged PR acme/widgets\#42: cancelled 2/3 active task(s), 1 no longer active
# [webhook] Merged PR acme/widgets\#42: cancelled 2/3 active task(s), 1 stopped without a durable record
# [webhook] Event received: pull_request, action: closed, repo: acme/widgets, delivery: delivery-oosr2junwbq
# [webhook] Merged PR acme/widgets\#42: cancelled 3/3 active task(s)
# [webhook] Event received: pull_request, action: closed, repo: acme/widgets, delivery: delivery-3ykttnwg359
# [webhook] Event received: pull_request, action: closed, repo: acme/widgets, delivery: delivery-8x5njyhizuf
# [webhook] Merge-triggered task cancellation failed {
#   repository: 'acme/widgets',
#   prNumber: 42,
#   error: Error: database unavailable
#       at Object.getActiveTasksForPR (/home/runner/work/propr/propr/packages/api/test/mergedPrTaskCancellation.test.ts:202:46)
#       at cancelActiveTasksForMergedPR (/home/runner/work/propr/propr/packages/api/webhookHandler.ts:187:50)
#       at handleWebhookRequest (/home/runner/work/propr/propr/packages/api/webhookHandler.ts:333:28)
#       at async TestContext.<anonymous> (/home/runner/work/propr/propr/packages/api/test/mergedPrTaskCancellation.test.ts:207:3)
#       at async Test.run (node:internal/test_runner/test:1054:7)
#       at async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
# }
# Subtest: isMergedPullRequestClose only matches merged PR close events
ok 1 - isMergedPullRequestClose only matches merged PR close events
  ---
  duration_ms: 1.257141
  type: 'test'
  ...
# Subtest: cancelActiveTasksForMergedPR stops every active task and queued job with the merge reason
ok 2 - cancelActiveTasksForMergedPR stops every active task and queued job with the merge reason
  ---
  duration_ms: 2.83435
  type: 'test'
  ...
# Subtest: cancelActiveTasksForMergedPR does nothing when there is no active work
ok 3 - cancelActiveTasksForMergedPR does nothing when there is no active work
  ---
  duration_ms: 0.899422
  type: 'test'
  ...
# Subtest: cancelActiveTasksForMergedPR continues cancelling after a task fails to stop
ok 4 - cancelActiveTasksForMergedPR continues cancelling after a task fails to stop
  ---
  duration_ms: 3.564967
  type: 'test'
  ...
# Subtest: cancelActiveTasksForMergedPR counts no-longer-active tasks as skipped
ok 5 - cancelActiveTasksForMergedPR counts no-longer-active tasks as skipped
  ---
  duration_ms: 0.49172
  type: 'test'
  ...
# Subtest: cancelActiveTasksForMergedPR surfaces stops without a durable cancellation record separately
ok 6 - cancelActiveTasksForMergedPR surfaces stops without a durable cancellation record separately
  ---
  duration_ms: 0.393455
  type: 'test'
  ...
# Subtest: handleWebhookRequest cancels active tasks when a PR is merged
ok 7 - handleWebhookRequest cancels active tasks when a PR is merged
  ---
  duration_ms: 1.642301
  type: 'test'
  ...
# Subtest: handleWebhookRequest does not cancel anything for an unmerged PR close
ok 8 - handleWebhookRequest does not cancel anything for an unmerged PR close
  ---
  duration_ms: 0.301223
  type: 'test'
  ...
# Subtest: handleWebhookRequest still succeeds when the PR task lookup fails entirely
ok 9 - handleWebhookRequest still succeeds when the PR task lookup fails entirely
  ---
  duration_ms: 0.912867
  type: 'test'
  ...
# [2026-09-04 08:06:15.463 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/021-mergedPrTaskCancellation.test.ts/propr.test.sqlite"
#     environment: "test"
1..9
# tests 9
# suites 0
# pass 9
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2226.463022

[22/327] packages/api/test/notificationManagementRoutes.test.ts
TAP version 13
# Subtest: notification subscription management routes
    # Subtest: lists safe metadata and revokes an owned opaque subscription ID
    ok 1 - lists safe metadata and revokes an owned opaque subscription ID
      ---
      duration_ms: 3.170248
      type: 'test'
      ...
    # Subtest: sanitizes preference snapshots at both response boundaries
    ok 2 - sanitizes preference snapshots at both response boundaries
      ---
      duration_ms: 0.960105
      type: 'test'
      ...
    # Subtest: warns distinctly and without secrets for invalid VAPID configuration
    ok 3 - warns distinctly and without secrets for invalid VAPID configuration
      ---
      duration_ms: 1.655706
      type: 'test'
      ...
    1..3
ok 1 - notification subscription management routes
  ---
  duration_ms: 6.888209
  type: 'suite'
  ...
# [2026-09-04 08:06:17.870 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/022-notificationManagementRoutes.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-09-04 08:06:17.871 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
1..1
# tests 3
# suites 1
# pass 3
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2292.32167

[23/327] packages/api/test/notificationProjectionRace.test.ts
TAP version 13
# Subtest: notification projection lifecycle races
    # Subtest: replaces an older PR-attention card while preserving both audit events
    ok 1 - replaces an older PR-attention card while preserving both audit events
      ---
      duration_ms: 167.158862
      type: 'test'
      ...
    # Subtest: does not recreate PR notifications after the durable merge transition
    ok 2 - does not recreate PR notifications after the durable merge transition
      ---
      duration_ms: 116.973602
      type: 'test'
      ...
    1..2
ok 1 - notification projection lifecycle races
  ---
  duration_ms: 285.490914
  type: 'suite'
  ...
# [2026-09-04 08:06:20.271 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/023-notificationProjectionRace.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-09-04 08:06:20.553 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
1..1
# tests 2
# suites 1
# pass 2
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2889.778445

[24/327] packages/api/test/notificationProjectionService.test.ts
TAP version 13
# Subtest: notification lifecycle projection
    # Subtest: creates exactly one plan-ready event for the draft owner
    ok 1 - creates exactly one plan-ready event for the draft owner
      ---
      duration_ms: 153.072141
      type: 'test'
      ...
    # Subtest: emits one descriptive notification for each completed task
    ok 2 - emits one descriptive notification for each completed task
      ---
      duration_ms: 134.54053
      type: 'test'
      ...
    # Subtest: ignores stale task transitions and emits one stalled event per unchanged activity
    ok 3 - ignores stale task transitions and emits one stalled event per unchanged activity
      ---
      duration_ms: 124.719887
      type: 'test'
      ...
    # Subtest: projects a task failure once without copying error details
    ok 4 - projects a task failure once without copying error details
      ---
      duration_ms: 122.355255
      type: 'test'
      ...
    # Subtest: does not advertise Open PR when a trusted GitHub URL cannot be constructed
    ok 5 - does not advertise Open PR when a trusted GitHub URL cannot be constructed
      ---
      duration_ms: 107.803816
      type: 'test'
      ...
    # Subtest: advertises follow-up only with the stored repository and issue identity the endpoint requires
    ok 6 - advertises follow-up only with the stored repository and issue identity the endpoint requires
      ---
      duration_ms: 130.840071
      type: 'test'
      ...
    # Subtest: advertises review follow-up only when the endpoint issue is the reviewed PR
    ok 7 - advertises review follow-up only when the endpoint issue is the reviewed PR
      ---
      duration_ms: 116.277075
      type: 'test'
      ...
    # Subtest: restricts indexing failures to administrators
    ok 8 - restricts indexing failures to administrators
      ---
      duration_ms: 120.700812
      type: 'test'
      ...
    # Subtest: deduplicates system failures across instances and dismisses them on recovery
    ok 9 - deduplicates system failures across instances and dismisses them on recovery
      ---
      duration_ms: 161.567659
      type: 'test'
      ...
    # Subtest: logs and isolates projection persistence failures
    ok 10 - logs and isolates projection persistence failures
      ---
      duration_ms: 102.003048
      type: 'test'
      ...
    1..10
ok 1 - notification lifecycle projection
  ---
  duration_ms: 1275.623162
  type: 'suite'
  ...
# [2026-09-04 08:06:23.240 +0000] �[32mINFO�[39m: �[36mSQLite database connection established successfully�[39m
#     filename: "/tmp/propr-test-suite-dVcmep/024-notificationProjectionService.test.ts/propr.test.sqlite"
#     environment: "test"
# [2026-09-04 08:06:24.513 +0000] �[32mINFO�[39m: �[36mSQLite database connection closed�[39m
1..1
# tests 10
# suites 1
# pass 10
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 3751.138576

[25/327] packages/api/test/notificationRoutes.test.ts
TAP version 13
# Subtest: notification routes
    # Subtest: uses only the authenticated user and clamps list limits
    ok 1 - uses only the authenticated user and clamps list limits
      ---
      duration_ms: 3.247422
      type: 'test'
      ...
    # Subtest: uses the authenticated user for receipt mutations
    ok 2 - uses the authenticated user for receipt mutations
      ---
      duration_ms: 1.509984
      type: 'test'
      ...
    # Subtest: dismisses all receipts for the authenticated user
    ok 3 - dismisses all receipts for the authenticated user
      ---
      duration_ms: 1.060914
      type: 'test'
      ...
    # Subtest: returns 400 for malformed limits, cursors, and history flags
    ok 4 - returns 400 for malformed limits, cursors, and history flags
  

...(truncated)

View Workflow

@propr-dev

propr-dev Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Failed to apply follow-up changes requested by @github-actions[bot]

An error occurred while processing your request:

fatal: couldn't find remote ref refs/heads/visual-previews-b1b77b35


Comment ID: 5537687536✓
Please check the logs for more details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants