Compare commits

..
Author SHA1 Message Date
forge-recovery 93cd228188 Merge pull request 'ci(installer): skip MSI ICE validation only when the Windows Installer service is unreachable' (#26) from ci/msi-fallback-on-shared-windows into ci/shared-windows
build / scripts (push) Successful in 34s
windows / windows (push) Successful in 4m54s
node / node (push) Successful in 34s
build / windows (push) Successful in 4m54s
2026-11-10 20:21:05 +00:00
james.maesandClaude Sonnet 5.5 a36f9105dd ci(installer): skip MSI ICE validation only when the Windows Installer service is unreachable
build / scripts (push) Successful in 33s
windows / windows (push) Successful in 5m20s
node / node (push) Successful in 33s
build / windows (push) Successful in 5m20s
WiX validates the MSI through the Windows Installer service (msiserver). The shared native
Windows runners log on as batch accounts, which Windows does not allow to start that service,
so every ICE action fails with WIX0217 and the whole MSI build fails after all tests pass.

Try to reach the service before the MSI build. When it is reachable, validation runs exactly as
before. When it is not, build without validation and warn loudly. A signed release build (one
with a certificate thumbprint) refuses to skip validation and fails instead, so it must be built
on a host where validation works.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-10 12:26:38 -06:00
james.maesandClaude Sonnet 5.5 458733bca8 ci: remove temporary msiserver diagnostics
build / scripts (push) Successful in 34s
windows / windows (push) Failing after 4m43s
node / node (push) Successful in 34s
build / windows (push) Failing after 4m43s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 07:52:25 -06:00
james.maes 46999ea21c ci: temporary msiserver diagnostics (2)
build / scripts (push) Successful in 34s
windows / windows (push) Canceled after 15m8s
node / node (push) Successful in 34s
build / windows (push) Canceled after 15m8s
2026-11-09 07:36:15 -06:00
james.maesandClaude Sonnet 5.5 c3b7c37f47 test: widen hang-guard timeouts in the Cli and Core suites for loaded CI hosts
build / scripts (push) Successful in 32s
windows / windows (push) Failing after 2m33s
node / node (push) Successful in 32s
build / windows (push) Failing after 2m33s
The guards only bound a hung test; they are not behavior under test. Isolated runs finish in 1-2 s,
but shared runners starved tests past 10 s (two different tests failed in two consecutive runs).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 07:32:30 -06:00
james.maesandClaude Sonnet 5.5 af9e2f99e0 test(cli): widen the hang guard of the control concurrency test for loaded CI hosts
build / scripts (push) Successful in 35s
windows / windows (push) Failing after 2m10s
node / node (push) Successful in 35s
build / windows (push) Failing after 2m10s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 07:27:23 -06:00
james.maes 84a1002143 ci: temporary msiserver diagnostics
build / scripts (push) Successful in 33s
windows / windows (push) Failing after 2m29s
node / node (push) Successful in 33s
build / windows (push) Failing after 2m29s
2026-11-09 07:22:25 -06:00
James MaesandClaude Opus 5.5 8689dd593c ci: run the Windows job on the Lore QRun runner (qqq-windows-amd64)
build / scripts (push) Successful in 35s
windows / windows (push) Failing after 4m14s
node / node (push) Successful in 35s
build / windows (push) Failing after 4m14s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:17:07 -06:00
James MaesandClaude Opus 5.5 a2f90fb1fb ci: pin the shared workflow to v1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:46:16 -06:00
James MaesandClaude Opus 5.5 bed2959293 ci: let the Pester suites see child exit codes
build / scripts (push) Successful in 40s
windows / windows (push) Failing after 1m27s
node / node (push) Successful in 40s
build / windows (push) Failing after 1m27s
They assert on failing child processes themselves; the shared workflow's
native-exit-code preference turned those expected failures into errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:42:19 -06:00
James MaesandClaude Opus 5.5 e0bc919c3c ci: create the module directory before saving the pinned modules
build / scripts (push) Successful in 48s
windows / windows (push) Failing after 1m3s
node / node (push) Successful in 48s
build / windows (push) Failing after 1m3s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:38:27 -06:00
James MaesandClaude Opus 5.5 d1517048f3 ci: build on the shared ci-workflows Windows workflow
build / scripts (push) Successful in 34s
windows / windows (push) Failing after 1m13s
node / node (push) Successful in 34s
build / windows (push) Failing after 1m13s
Locked restore/build/tests with the pinned protocol fixtures, SDK inventory,
branded installer and packaging checks, the unsigned MSIs and the PowerShell
analyzer/Pester checks run on the QRun-IO Windows runner; v* tags attach the
installers and SHA256SUMS to the Gitea release. Script quality, source
headers, typecheck, lint and the virtual-agent tests run on node.yaml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:35:38 -06:00
james.maes adf885beaf Merge pull request 'docs(changelog): restore the 1.1.1 heading; record 1.2.1 in TODO' (#15) from docs/changelog-1.1.1-heading into main
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: Shared .NET CI / portable (push) Skipped
QRun-IO/ci-policy: Shared source and config CI / gate (push) Failing after 1m54s
QRun-IO/ci-policy: ci / source-config-14 (push) Failing after 2m30s
QRun-IO/ci-policy: ci / ci-result (push) Failing after 8s
QRun-IO/ci-policy: native / dotnet-0 (push) Successful in 20m4s
QRun-IO/ci-policy: native / native-result (push) Successful in 5s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 9cb8c13124a99d18da83cbcd4faa795859ac9e55] (push) Successful in 13s
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared .NET CI / secrets (push) Successful in 30s
QRun-IO/ci-policy: Shared .NET CI / verify (push) Skipped
QRun-IO/ci-policy: Shared source and config CI / verify (push) Successful in 22s
QRun-IO/ci-policy: Shared source and config CI / secrets (push) Successful in 33s
QRun-IO/ci-policy: Shared .NET CI / hosted (push) Successful in 19m50s
QRun-IO/ci-policy: Shared .NET CI / gate (push) Successful in 12s
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
2026-11-06 23:23:40 +00:00
james.maes c4b14963d3 Merge pull request 'docs(handoff): point the Forge printing cohort at QBit 1.1.2 on QQQ RC.22 from the Gitea registry' (#16) from docs/forge-handoff-qbit-112-20261106 into main
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: Shared .NET CI / portable (push) Skipped
QRun-IO/ci-policy: Shared source and config CI / gate (push) Successful in 22s
QRun-IO/ci-policy: ci / source-config-14 (push) Successful in 45s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 9s
QRun-IO/ci-policy: native / dotnet-0 (push) Failing after 9m42s
QRun-IO/ci-policy: native / native-result (push) Failing after 8s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (push) Successful in 11s
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared .NET CI / secrets (push) Successful in 25s
QRun-IO/ci-policy: Shared .NET CI / verify (push) Skipped
QRun-IO/ci-policy: Shared source and config CI / verify (push) Successful in 20s
QRun-IO/ci-policy: Shared source and config CI / secrets (push) Successful in 21s
QRun-IO/ci-policy: Shared .NET CI / hosted (push) Failing after 9m19s
QRun-IO/ci-policy: Shared .NET CI / gate (push) Failing after 20s
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
2026-11-06 20:59:13 +00:00
james.maesandClaude Fable 5.1 a586ea7bf8 docs(handoff): point the Forge printing cohort at QBit 1.1.2 on QQQ RC.22 from the Gitea registry
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (pull_request) Successful in 15s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: Shared .NET CI / verify (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: Shared source and config CI / verify (pull_request) Successful in 24s
QRun-IO/ci-policy: Shared source and config CI / secrets (pull_request) Successful in 36s
QRun-IO/ci-policy: Shared source and config CI / gate (pull_request) Successful in 29s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 11s
QRun-IO/ci-policy: Shared .NET CI / hosted (pull_request) Successful in 10m50s
QRun-IO/ci-policy: Shared .NET CI / gate (pull_request) Successful in 24s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: Shared .NET CI / secrets (pull_request) Successful in 26s
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: Shared .NET CI / portable (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Successful in 1m11s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Successful in 11m22s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
Same code as 1.1.1, certified on 4.1.0-RC.22 (Forge's core pin) and published by the
shared pipeline; exact tag, builds, registry coordinate and verified hashes recorded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 14:45:48 -06:00
james.maes 9906002d65 Merge pull request 'docs(handoff): point the Forge printing cohort at QBit 1.1.1' (#14) from docs/forge-handoff-qbit-111-20261106 into main
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: Shared .NET CI / portable (push) Skipped
QRun-IO/ci-policy: Shared source and config CI / gate (push) Successful in 32s
QRun-IO/ci-policy: ci / source-config-14 (push) Successful in 1m18s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 10s
QRun-IO/ci-policy: native / dotnet-0 (push) Successful in 14m1s
QRun-IO/ci-policy: native / native-result (push) Successful in 7s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (push) Successful in 10s
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: Shared .NET CI / secrets (push) Successful in 21s
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared .NET CI / verify (push) Skipped
QRun-IO/ci-policy: Shared source and config CI / verify (push) Successful in 20s
QRun-IO/ci-policy: Shared source and config CI / secrets (push) Successful in 38s
QRun-IO/ci-policy: Shared .NET CI / hosted (push) Successful in 13m46s
QRun-IO/ci-policy: Shared .NET CI / gate (push) Successful in 14s
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
2026-11-06 16:50:07 +00:00
james.maesandClaude Fable 5.1 f7a9f79ab5 docs(changelog): restore the 1.1.1 heading; record 1.2.1 in TODO
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: Shared source and config CI / secrets (pull_request) Successful in 26s
QRun-IO/ci-policy: Shared source and config CI / verify (pull_request) Successful in 31s
QRun-IO/ci-policy: Shared .NET CI / verify (pull_request) Skipped
QRun-IO/ci-policy: Shared source and config CI / gate (pull_request) Successful in 20s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 9s
QRun-IO/ci-policy: Shared .NET CI / hosted (pull_request) Successful in 12m20s
QRun-IO/ci-policy: Shared .NET CI / gate (pull_request) Successful in 18s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: Shared .NET CI / secrets (pull_request) Successful in 45s
QRun-IO/ci-policy: Shared .NET CI / portable (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Successful in 57s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Successful in 12m40s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
The editions branch folded the published 1.1.1 release's browser sign-in
fix into the 1.2.0 CHANGELOG entry. Put the `## 1.1.1` heading back so the
changelog matches the published releases. Refresh docs/TODO.md: 1.2.1 is
the current stable, the v1.2.0 sign-job failure is closed by PR 12, and
the nonce section is historical. Docs only; no source or workflow change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 10:41:24 -06:00
james.maesandClaude Fable 5.1 da43fa2918 docs(handoff): point the Forge printing cohort at QBit 1.1.1
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: Shared .NET CI / verify (pull_request) Skipped
QRun-IO/ci-policy: Shared source and config CI / verify (pull_request) Successful in 46s
QRun-IO/ci-policy: Shared source and config CI / secrets (pull_request) Successful in 46s
QRun-IO/ci-policy: Shared source and config CI / gate (pull_request) Successful in 26s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 14s
QRun-IO/ci-policy: Shared .NET CI / hosted (pull_request) Successful in 13m57s
QRun-IO/ci-policy: Shared .NET CI / gate (pull_request) Successful in 19s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: Shared .NET CI / secrets (pull_request) Successful in 40s
QRun-IO/ci-policy: Shared .NET CI / portable (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Successful in 1m24s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Successful in 14m19s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
1.1.1 supersedes 1.1.0 (realtime upgrade header-case fix, no contract change).
Exact tag, run, registry coordinates and verified JAR/POM hashes recorded for the
Forge agent; pairing matrix updated to name 1.1.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 10:34:31 -06:00
james.maes b5831302d7 Merge pull request 'docs(release): record 1.2.1 as current stable; Forge handoff' (#13) from docs/release-1.2.1-20261106 into main
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Successful in 14m12s
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 7s
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: Shared .NET CI / portable (push) Skipped
QRun-IO/ci-policy: Shared source and config CI / gate (push) Successful in 10s
QRun-IO/ci-policy: ci / source-config-14 (push) Successful in 28s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 5s
QRun-IO/ci-policy: Shared .NET CI / hosted (push) Successful in 13m46s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (push) Successful in 7s
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: Shared .NET CI / gate (push) Successful in 19s
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: Shared .NET CI / secrets (push) Successful in 14s
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared .NET CI / verify (push) Skipped
QRun-IO/ci-policy: Shared source and config CI / verify (push) Successful in 12s
QRun-IO/ci-policy: Shared source and config CI / secrets (push) Successful in 14s
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
2026-11-06 14:09:03 +00:00
james.maesandClaude Fable 5.1 22b942b5e8 docs(readme): current stable 1.2.1 with the 1.1.0 server and protocol cohort
QRun-IO/ci-policy: Shared .NET CI / verify (pull_request) Skipped
QRun-IO/ci-policy: Shared .NET CI / hosted (pull_request) Successful in 12m12s
QRun-IO/ci-policy: Shared source and config CI / secrets (pull_request) Successful in 20s
QRun-IO/ci-policy: Shared .NET CI / gate (pull_request) Successful in 13s
QRun-IO/ci-policy: Shared source and config CI / gate (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: Shared source and config CI / verify (pull_request) Successful in 10s
QRun-IO/ci-policy: Shared .NET CI / portable (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Successful in 12m29s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Successful in 41s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 4s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: Shared .NET CI / secrets (pull_request) Successful in 13s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 07:54:09 -06:00
james.maesandClaude Fable 5.1 40872ab002 docs(release): record 1.2.1 as the current stable release and add the Forge handoff
QRun-IO/ci-policy: Shared .NET CI / hosted (pull_request) Successful in 10m41s
QRun-IO/ci-policy: Shared .NET CI / gate (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: Shared source and config CI / verify (pull_request) Successful in 12s
QRun-IO/ci-policy: Shared .NET CI / verify (pull_request) Skipped
QRun-IO/ci-policy: Shared source and config CI / secrets (pull_request) Successful in 11s
QRun-IO/ci-policy: Shared source and config CI / gate (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Successful in 10m51s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: Shared .NET CI / secrets (pull_request) Successful in 13s
QRun-IO/ci-policy: Shared .NET CI / portable (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Successful in 23s
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 07:31:00 -06:00
34 changed files with 230 additions and 60 deletions

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
name: build
on:
push:
branches: ['**']
tags: ['v*']
jobs:
windows:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/windows.yaml@v1
secrets: inherit
with:
runner: qqq-windows-amd64
commands: |
$temp = $env:RUNNER_TEMP ?? [IO.Path]::GetTempPath()
# Protocol fixtures pinned to the revision the verification expects.
$protocol = Join-Path $temp "qqq-print-protocol-$env:GITHUB_RUN_ID"
git clone --quiet https://git.kof22.com/QRun-IO/qqq-print-protocol.git $protocol
git -C $protocol checkout --quiet ac835f8b9396b641b525669249fdb4a1e2a50c99
pwsh -NoProfile -File ./build/Verify.ps1 -ProtocolFixtures (Join-Path $protocol fixtures)
pwsh -NoProfile -File ./build/Test-WindowsSdkInventory.ps1 -VerifyPackages
pwsh -NoProfile -File ./build/Test-BrandedInstaller.ps1
pwsh -NoProfile -File ./build/Test-PackagingInputs.ps1
# Unsigned MSIs: the base product plus one branded installer per edition. A vX.Y.Z[-rc.N] tag sets the version.
$release = if ($env:GITHUB_REF_TYPE -eq 'tag') { $env:GITHUB_REF_NAME.TrimStart('v') } else { '1.2.1' }
$product = $release -replace '-.*$', ''
pwsh -NoProfile -File ./build/Build-Installer.ps1 -ProductVersion $product -ReleaseVersion $release
$installers = @('QqqPrintAgent')
foreach ($folder in Get-ChildItem -LiteralPath editions -Directory | Sort-Object Name) {
pwsh -NoProfile -File ./build/Build-Installer.ps1 -ProductVersion $product -ReleaseVersion $release -Edition $folder.Name
$installers += (Get-Content -LiteralPath (Join-Path $folder.FullName 'edition.json') -Raw | ConvertFrom-Json).assetPrefix
}
New-Item -ItemType Directory -Path artifacts/release -Force | Out-Null
foreach ($prefix in $installers) {
Copy-Item -LiteralPath "artifacts/installer/$prefix-$product-x64.msi" -Destination "artifacts/release/$prefix-v$release-x64.msi"
}
Copy-Item -LiteralPath build/Install-Branded.ps1, docs/branding.example.json -Destination artifacts/release/
# PowerShell scripts: analyzer and Pester rejection-path tests with pinned modules saved to the job's temp directory.
$modules = Join-Path $temp "psmodules-$env:GITHUB_RUN_ID"
New-Item -ItemType Directory -Force -Path $modules | Out-Null
Save-PSResource -Name PSScriptAnalyzer -Version 1.25.0 -Repository PSGallery -TrustRepository -Path $modules
Save-PSResource -Name Pester -Version 5.7.1 -Repository PSGallery -TrustRepository -Path $modules
$env:PSModulePath = "$modules$([IO.Path]::PathSeparator)$env:PSModulePath"
pwsh -NoProfile -File ./build/Analyze-Scripts.ps1
Import-Module Pester -RequiredVersion 5.7.1
# The Pester suites assert on child processes' exit codes themselves.
$PSNativeCommandUseErrorActionPreference = $false
$pester = New-PesterConfiguration
$pester.Run.Path = @('build/Assert-Coverage.Tests.ps1', 'build/Analyze-Scripts.Tests.ps1', 'build/Install-Branded.Tests.ps1', 'build/Test-WindowsSdkInventory.Tests.ps1')
$pester.Run.PassThru = $true
if ((Invoke-Pester -Configuration $pester).Result -ne 'Passed') { throw 'Pester tests failed' }
artifacts: |
artifacts/release/*.msi
artifacts/release/Install-Branded.ps1
artifacts/release/branding.example.json
scripts:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/node.yaml@v1
with:
node-version: '24'
uv: true
install: npm ci --ignore-scripts --no-audit --no-fund
commands: |
uv sync --locked
uv run --locked --no-sync python .github/scripts/test-script-quality.py
uv run --locked --no-sync python .github/scripts/check-script-quality.py
npm run typecheck
npm run lint
python3 .github/scripts/test-source-headers.py
python3 .github/scripts/check-source-headers.py
node --test tools/virtual-agent/test/virtual-agent.test.mjs
+2
View File
@@ -36,6 +36,8 @@ reset with the connection. The activity log viewer has a Clear logs button, the
result banner above the form and shows a waiting state during browser sign-in, and the service's sign-in reply
no longer repeats itself. No wire change.
## 1.1.1
Browser sign-in fix. The companion runs elevated, and its callback inbox pipe was keyed with
PipeOptions.CurrentUserOnly to the elevated token's default owner (the Administrators group), so
the protocol handler the browser launches as the ordinary user was refused with "Access to the
+5 -5
View File
@@ -8,13 +8,13 @@
Print from **QQQ 4.1+** to local Windows printers without opening inbound network ports. The Windows service opens an authenticated two-way WebSocket to QQQ for commands, status and job notifications. Documents download separately over HTTPS, so a large transfer does not block the control connection. There is no job/control polling fallback.
**Current stable release: [1.0.1](https://git.kof22.com/QRun-IO/qqq-print-agent/releases/tag/v1.0.1).** The service, companion, CLI and MSI are signed with Azure Artifact Signing. The matching server release is [qbit-printing 1.0.0](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.0.0), using QQQ **4.1.0-RC.21** for native session login, with [protocol 1.0.0](https://git.kof22.com/QRun-IO/qqq-print-protocol/releases/tag/v1.0.0). Product versions are separate from native printing API **2.3**, wire payload **v1** and WebSocket subprotocol `qqq-printing.v1`.
**Current stable release: [1.2.1](https://git.kof22.com/QRun-IO/qqq-print-agent/releases/tag/v1.2.1)** (generic and GG Forge edition installers). The service, companion, CLI and MSI are signed with Azure Artifact Signing. The matching server release is [qbit-printing 1.1.0](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.1.0), using QQQ **4.1.0-RC.21** for native session login, with [protocol 1.1.0](https://git.kof22.com/QRun-IO/qqq-print-protocol/releases/tag/v1.1.0); 1.2.1 also works unchanged against qbit-printing 1.0.0. Product versions are separate from native printing API **2.4** (2.3 compatible), wire payload **v1** and WebSocket subprotocol `qqq-printing.v1`.
The setup/status application uses the same [KofTwentyTwo AppKit](https://git.kof22.com/KofTwentyTwo/AppKit) foundation as GCLO and MarkdownViewr.Windows. QQQ's mascot Query appears in About and branded warehouse test documents.
## Install and connect
1. Download `QqqPrintAgent-v1.0.1-x64.msi` and `SHA256SUMS` from the [stable release](https://git.kof22.com/QRun-IO/qqq-print-agent/releases/tag/v1.0.1). Check the checksum and Authenticode signature as described in [Installation](docs/INSTALLATION.md).
1. Download `QqqPrintAgent-v1.2.1-x64.msi` (or a client edition such as `GgForgePrintAgent-v1.2.1-x64.msi`; one edition per PC) and `SHA256SUMS` from the [stable release](https://git.kof22.com/QRun-IO/qqq-print-agent/releases/tag/v1.2.1). Check the checksum and Authenticode signature as described in [Installation](docs/INSTALLATION.md).
2. Install with administrator rights. The MSI installs the automatic Windows service and adds **QQQ Print Agent** under **Print Connectors** in Start.
3. Open the app and accept its administrator prompt. Choose **Settings → Server connection → Sign in / Connection settings**, enter the QQQ HTTPS server address and select **Discover sign-in options**. Sign in using the advertised username/password or browser flow.
4. For a new device, create an enrollment code in QQQ under **Printing → Print devices → Actions → Enroll a print device**, then enter it in the client. Existing pairings are reused or explicitly associated with the signed-in account; routine upgrades do not need a replacement pairing.
@@ -55,7 +55,7 @@ The client reports the Windows queue's machine-default media as a revision-bound
## Requirements and limits
| Area | Stable 1.0.1 behavior |
| Area | Stable 1.2.1 behavior |
| --- | --- |
| Server | QQQ 4.1+; recommended cohort QQQ 4.1.0-RC.21 and qbit-printing 1.0.0, native printing API 2.3 |
| Windows | x64; Windows 11 used for hardware acceptance; API target Windows 10 build 19041+ |
@@ -87,7 +87,7 @@ git clone https://git.kof22.com/QRun-IO/qqq-print-protocol.git
git -C qqq-print-protocol checkout 42c0d249f81395f00408ec0f92638dcbbf5deb03
Set-Location qqq-print-agent
./build/Verify.ps1
./build/Build-Installer.ps1 -ProductVersion 1.0.1 -ReleaseVersion 1.0.1
./build/Build-Installer.ps1 -ProductVersion 1.2.1 -ReleaseVersion 1.2.1
```
Run verification only on a permitted Windows test host; host-specific restrictions remain in force. Local packaging is unsigned unless explicitly signed; stable release signing runs through the protected workflow. Neither command installs the service or sends a physical print.
@@ -96,7 +96,7 @@ Run verification only on a permitted Windows test host; host-specific restrictio
## Release evidence
The signed 1.0.1 release passed its protected build, test, security, CodeQL, script and signing workflow. Published checksums, signatures and provenance were verified. The local 1.0.1 MSI upgrade completed successfully on 2026-11-03 with matching app/service/CLI versions and a running service. Dated hardware and earlier upgrade evidence remains in [Acceptance](docs/ACCEPTANCE.md); current release status is in [Release readiness](docs/RELEASE-READINESS.md). Earlier test results retain their original version and scope.
The signed 1.2.1 release passed its protected build, test, security, CodeQL, script and per-edition signing workflow (run 42223525626); checksums, Authenticode signatures and provenance attestations were verified on the published generic and GG Forge installers, and the GG Forge installer was upgraded in place on the development PC with pairing, session and settings preserved. The earlier signed 1.0.1 release passed its protected build, test, security, CodeQL, script and signing workflow. Published checksums, signatures and provenance were verified. The local 1.0.1 MSI upgrade completed successfully on 2026-11-03 with matching app/service/CLI versions and a running service. Dated hardware and earlier upgrade evidence remains in [Acceptance](docs/ACCEPTANCE.md); current release status is in [Release readiness](docs/RELEASE-READINESS.md). Earlier test results retain their original version and scope.
## License and acknowledgments
+24 -1
View File
@@ -188,7 +188,30 @@ if($CertificateThumbprint)
}
# WiX incremental inputs do not reliably invalidate a changed version/payload path.
& dotnet build $installerProject --configuration Release -t:Rebuild '-p:RestoreLockedMode=true' "-p:ProductVersion=$ProductVersion" "-p:PayloadRoot=$payloadRoot" "-p:OutputPath=$buildDirectory\" @brandInstallerArgs
# WiX validates the MSI (ICE checks) through the Windows Installer service. A runner that logs on as a batch account cannot
# open that service, so WiX fails every ICE action with WIX0217 ("The Windows Installer Service could not be accessed").
# Validate whenever the service is reachable. Skip validation only when it is not, with a loud warning, and never for a
# signed release build: that must be built on a host where validation works.
$installerValidationArguments = @()
try
{
$installerService = Get-Service -Name msiserver -ErrorAction Stop
if($installerService.Status -ne 'Running')
{
$installerService.Start()
$installerService.WaitForStatus('Running', [TimeSpan]::FromSeconds(30))
}
}
catch
{
if($CertificateThumbprint)
{
throw "The Windows Installer service is not reachable, so the MSI cannot be validated; refusing to build a signed release without validation. $($_.Exception.Message)"
}
Write-Warning "The Windows Installer service is not reachable from this session; building the MSI WITHOUT ICE validation (not a signed release build). $($_.Exception.Message)"
$installerValidationArguments = @('-p:SuppressValidation=true')
}
& dotnet build $installerProject --configuration Release -t:Rebuild '-p:RestoreLockedMode=true' "-p:ProductVersion=$ProductVersion" "-p:PayloadRoot=$payloadRoot" "-p:OutputPath=$buildDirectory\" @brandInstallerArgs @installerValidationArguments
if($LASTEXITCODE -ne 0) { throw 'MSI build failed.' }
$builtMsi = Join-Path $buildDirectory "$msiBaseName-$ProductVersion-x64.msi"
if(-not (Test-Path -LiteralPath $builtMsi)) { throw 'MSI output was not produced.' }
+62
View File
@@ -0,0 +1,62 @@
# Printing cohort handoff for the Forge agent — 2026-11-05
Reusable changes were made in the printer client and the printing QBit repositories. Existing
authentication, discovery, enrollment, inventory and printing contracts are preserved; there are
**no breaking changes**. Nothing was installed over Forge's working client or dependencies; adoption,
pinning and the pairing tests (browser sign-in, session resume, enrollment, server printing,
TC22 → Echo → Z2) are yours. The previous cohort stays available for rollback.
## Exact versions and artifacts
| Component | Version | Tag / source | Artifact | SHA-256 |
| --- | --- | --- | --- | --- |
| Protocol (contract + fixtures) | 1.1.0 | `v1.1.0` at `5005f3c9e27053b6c327bb923695012832c0fdcc` | https://git.kof22.com/QRun-IO/qqq-print-protocol/releases/tag/v1.1.0 — `qqq-print-protocol-1.1.0.tar.gz` | `541f0f3054cbfe9ce16e7dea58fad01b55a7f0e9246ade8f30df1bb826940703` |
| Printing QBit (server) | **1.1.2**, native printing extension **2.4.0**, on QQQ **4.1.0-RC.22** from the Gitea registry (1.1.1 is the same code on RC.21 from Maven Central; 1.1.0 is superseded: do not adopt it) | `v1.1.2` at `f5639a98be9a2f15d4c61cf0955fe4ecef85824f`, push build 6435 + tag build 6475 | Maven `com.kingsrook.qbits:qbit-printing:1.1.2` from `https://git.kof22.com/api/packages/QRun-IO/maven` (repository id `qrun-io`, reader token); same bytes on https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.1.2 | JAR `786e6e570c5df29b01529562efb246a4f4562bb1d9fff6bea77d264e0d7536ce`, POM `7691a4e1cb7b03c7a559b0e2b92eacd92f86545b98bddafac86fc70f8602b03d` |
| Windows print client | **1.2.1** (1.2.0 content + release-pipeline fix; tag v1.2.0 produced no installer) | `v1.2.1` at `3b9d75d18f9cc8b80f4cf6d5aae81cccf8d61243`, GitHub release run 42223525626 | Signed installers: `QqqPrintAgent-v1.2.1-x64.msi` (generic) and `GgForgePrintAgent-v1.2.1-x64.msi` (GG Forge edition), `SHA256SUMS`, on https://github.com/QRun-IO/qqq-print-agent/releases/tag/v1.2.1 and byte-identical on https://git.kof22.com/QRun-IO/qqq-print-agent/releases/tag/v1.2.1 | generic `09920872b3447f8679b404f2b5ebd7d9fe82204b8007db63d58e4287f436f2eb`, GG Forge `5ce489092c1403449174b8a35eda039cd452e8685cbfe436d99aea3e15a5d8b4`; Authenticode valid (CN=James Maes, Microsoft timestamp); GitHub provenance attestation verified |
Previous cohort for rollback: protocol 1.0.0, QBit 1.0.0 (extension 2.3.0), Windows 1.0.1 / 1.1.1.
**2026-11-06 update (QBit 1.1.1).** Forge's qualification of 1.1.0 found that the realtime upgrade fails before WELCOME behind an ingress that lowercases header names (the device id was read case-sensitively from the WebSocket context; the same defect exists in 1.0.0, which Forge ran with a local patch). 1.1.1 resolves the device once on the authorized upgrade and carries it on the connection, with a regression test that connects with lowercase header names. No contract change: the pairing matrix and everything else in this handoff is unchanged; replace the `1.0.0-forge…headers1` coordinate with the published 1.1.1 and drop the local patch.
**2026-11-06 update (QBit 1.1.2, Gitea-only runtime).** James's direction: every QRun artifact resolves from the QRun-IO Gitea registry, no more Maven Central. 1.1.2 is the 1.1.1 code certified on QQQ **4.1.0-RC.22** (the core version Forge already pins; it exists only in the Gitea registry) with `-Pui-demo verify`, 200 tests, 0 failures, and published by the shared `ci-workflows` pipeline. `qqq-frontend-next` 1.0.0-RC.37 is also mirrored into the registry byte-for-byte from Central. Pin `com.kingsrook.qbits:qbit-printing:1.1.2` together with core 4.1.0-RC.22 and read both from `https://git.kof22.com/api/packages/QRun-IO/maven`; third-party libraries still come through the Maven Central mirror until the registry proxies them. No contract change; pairing matrix unchanged.
## What changed, by contract
- **Printer approval over the realtime channel (extension 2.4.0, additive).** Client advertises
`printer-approval-v1` in HELLO only when discovery shows a stable printing 2.x with minor >= 4.
Server then sends a `PRINTERS` frame (`{ "printers": [ { "printerId", "enabled" } ] }`, the complete
approval state of the device's exposed printers) after each accepted INVENTORY and whenever an
administrator approves or disables one of that device's printers. Not acknowledged. A 2.3 peer on
either side keeps the exact 1.0 wire: 1.0.x/1.1.x clients never advertise it and never receive it;
the 1.2.1 client against QBit 1.0.0 reads the operator catalog instead (user-scoped; shows "Not
reported" without operator permission).
- **Remove printers / Remove device (QBit).** Archives with `removed_at`; hidden from lists, catalog,
claims and the PRINTERS frame; a re-shared printer returns as awaiting approval; retention purges
removed rows that no job references. QQQ 4.1 has no table-level default filter, so the raw Printers
/ Print devices tables still show removed rows until a "Removed at is empty" saved view is added.
- **Client sign-in nonce.** Sends `expectedNonce` under REQUIRED, OPTIONAL and the discovery capability
`nativeSessionNonceValidation`; legacy servers unchanged (PR 11 content, already merged earlier).
- **Client UX/ops.** Server column on the Printers page; verbose sign-in diagnostics with DEBUG level;
log size rollover; per-server untrusted-certificate option (development only, origin-bound);
connection dialog rework; Clear logs button in the log viewer; client editions (GG Forge build is a
separate MSI with its own update asset; one edition per PC, shared UpgradeCode).
## Pairing matrix (what must hold in your tests)
| Server | Client | Expected |
| --- | --- | --- |
| QBit 1.0.0 (2.3.0) | 1.2.1 | No `printer-approval-v1` in HELLO; catalog fallback; everything else as 1.1.1 |
| QBit 1.1.2 (2.4.0) | 1.0.1 / 1.1.1 | Four-capability HELLO at most; no PRINTERS frame; unchanged behaviour |
| QBit 1.1.2 (2.4.0) | 1.2.1 | Five-capability HELLO; PRINTERS after inventory and on approval change; Printers page shows Approved / Awaiting approval live |
## Evidence
- Protocol: `npm run check` (79 schema definitions, 82 fixtures), lint, typecheck, quality all green on 5005f3c.
- QBit: `mvnw -Pui-demo verify` 199 tests, 0 failures, 0 Checkstyle; main run 5473 verify + secrets + gate + post-publish green; registry files read back byte-for-byte.
- Client: hosted GitHub CI run 42116396114 on the PR head (197 Core + 362 Windows tests, 0 failures); Gitea required checks green on both merges; release run 42223525626 (verify, security, CodeQL, sign generic + GG Forge, assemble, publish).
## Notes for adoption
- Pin all three together. The client's CI fixture pin is `ac835f8b9396b641b525669249fdb4a1e2a50c99`; the QBit's is `5005f3c9e27053b6c327bb923695012832c0fdcc` (docs PRs 29 and 12 pending).
- Forge must approve the shared printers on the server after the first inventory from a 1.2.0 client, as before; the difference is that the client now shows that state.
- Coordinate installation on the Forge print station yourself; the GG Forge rc.3 build on James's PC is a local test build (file version 1.2.0.3) and will update to the signed 1.2.1 from its own GG Forge asset.
+1 -1
View File
@@ -2,7 +2,7 @@
Gitea is primary for source branches, reviews and normal pull requests. GitHub is the automatic source shadow. The existing authorized [GitHub release workflow](../.github/workflows/release.yml), its CI/security dependencies and the GitHub release assets remain the signing and installed-updater bridge. Do not retire those Windows workflows or remove the feed as part of ordinary source migration.
Current stable release: **[1.0.1 on Gitea](https://git.kof22.com/QRun-IO/qqq-print-agent/releases/tag/v1.0.1)** and the matching **[GitHub assets](https://github.com/QRun-IO/qqq-print-agent/releases/tag/v1.0.1)**. The protected [release run](https://github.com/QRun-IO/qqq-print-agent/actions/runs/41532214447) completed signing and publication. The earlier immutable `v1.0.0` and `v1.2.0` tags produced no signed installer (`v1.2.0`: the edition sign job's array splat, fixed in 1.2.1); they must not be moved or reused.
Current stable release: **[1.2.1 on Gitea](https://git.kof22.com/QRun-IO/qqq-print-agent/releases/tag/v1.2.1)** and the matching **[GitHub assets](https://github.com/QRun-IO/qqq-print-agent/releases/tag/v1.2.1)** (generic and GG Forge installers). The protected [release run](https://github.com/QRun-IO/qqq-print-agent/actions/runs/42223525626) completed signing and publication; checksums, Authenticode signatures and provenance attestations were verified on the downloaded assets (generic `09920872…`, GG Forge `5ce48909…`). The earlier immutable `v1.0.0` and `v1.2.0` tags produced no signed installer (`v1.2.0`: the edition sign job's array splat, fixed in 1.2.1); they must not be moved or reused. Previous stable: 1.1.1.
## Stable releases
+6 -1
View File
@@ -1,4 +1,9 @@
# Session state — 2026-11-05 (evening)
# Session state — 2026-11-06
## Current — 1.2.1 released; printing cohort protocol 1.1.0 / QBit 1.1.0 / client 1.2.1 complete
Client 1.2.1 (source `3b9d75d18f9cc8b80f4cf6d5aae81cccf8d61243`, GitHub release run 42223525626) is published with signed generic and GG Forge installers on GitHub and, byte-identical, on Gitea; see [RELEASING](RELEASING.md). Protocol 1.1.0 and QBit 1.1.0 (extension 2.4.0, including Remove printers / Remove device) are published. The handoff for the Forge agent, with every version, artifact and hash, is [FORGE-HANDOFF-2026-11-05.md](FORGE-HANDOFF-2026-11-05.md); Forge adoption, pairing tests and installation on its station are that lane's. The installed GG Forge rc.3 on this PC updates to 1.2.1 from its own asset through the in-app updater (not yet exercised). Open elsewhere: Gitea CI quota/runner-fleet work (Praetor lane); docs PRs qbit-printing 29 and qqq-print-protocol 12.
## 1.2.1 — release-pipeline fix after the v1.2.0 tag produced no signed installer
+12 -2
View File
@@ -1,6 +1,16 @@
# TODO
## Active: required-nonce native sign-in fix — 2026-11-05
## Released: 1.2.1 — 2026-11-06
Client 1.2.1 (source `3b9d75d`, GitHub release run 42223525626) is the current stable: signed generic and GG Forge installers on GitHub and, byte-identical, on Gitea. See [RELEASING](RELEASING.md).
- [x] Nonce fix PR 11 (`abc3d0d`) and the OPTIONAL/discovery nonce change (`c59814d`) shipped in `v1.2.0`/`v1.2.1`.
- [x] Release run 42128511730 on `v1.2.0` failed in `sign (gg-forge)`: `release.yml` array-splatted `-Edition`, binding it positionally to `-CertificateThumbprint`. Fixed in PR 12 (`3b9d75d`); `v1.2.0` stays immutable and unpublished.
- [x] `v1.2.1` tagged on the merge commit, mirrored to GitHub, signed, assembled and published; checksums, signatures and provenance verified; same assets attached to the Gitea release.
- [x] Post-merge main checks (ci, codeql, security, scripts, scorecard) green on `b583130`.
- [ ] Actual native sign-in/sign-out acceptance against the shared provider (the shared-fixture discovery pass is not acceptance).
## Historical: required-nonce native sign-in fix — 2026-11-05 (published in 1.2.x)
The stable printing release below remains accepted historical evidence. The shared-server scanner Authentik requires this fix (`fix/native-login-required-nonce`, integrated on main `f954f11`).
@@ -9,7 +19,7 @@ The stable printing release below remains accepted historical evidence. The shar
- [x] Integrated NativeCI Release build: zero warnings/errors; 352 Windows/CLI tests passed with no failures/skips; source notices passed for 300 tracked files. Existing method-length limit retained; Core.Tests excluded locally.
- [x] Hosted CI including Core.Tests: GitHub CI on the editions branch carrying this fix passed 182 Core and 352 Windows/CLI tests (run 42078731426, 2026-11-05).
- [ ] Real shared-provider native login acceptance.
- [ ] Final published build.
- [x] Final published build: 1.2.1 (see above).
## Closed product release — 2026-11-04
@@ -22,7 +22,7 @@ public sealed class AgentControlConcurrencyTests
using ControlFixture fixture = new();
fixture.OfferJob = true;
fixture.Printer.BlockPrint = true;
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(60)); // hang guard only: the test normally finishes in well under a second, but a loaded CI host can take seconds
using AgentRuntime runtime = fixture.Runtime();
Task running = runtime.RunAsync(stop.Token);
try
@@ -34,7 +34,7 @@ public sealed class AgentNewPairingTests
ChangeOldConfiguration(fixture, unreadable);
byte[] retained = File.ReadAllBytes(path);
fixture.NextSession = UnboundSession();
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
using(AgentRuntime runtime = fixture.Runtime())
{
_ = await runtime.SignInAsync(new PipeRequest("sign-in", SessionCredential: fixture.NextSession.ExportCredential(), StartNewPairing: true), timeout.Token);
@@ -77,7 +77,7 @@ public sealed class AgentNewPairingTests
string path = Path.Combine(fixture.Local.DirectoryPath, "connection.dpapi");
byte[] retained = File.ReadAllBytes(path);
using AgentRuntime runtime = fixture.Runtime();
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
_ = await runtime.PollControlsOnceAsync(timeout.Token);
Assert.False(runtime.GetSnapshot().Authentication!.Authorized);
Assert.Equal(0, fixture.Created);
@@ -119,7 +119,7 @@ public sealed class AgentNewPairingTests
string authPath = Path.Combine(fixture.Local.DirectoryPath, "qqq-session.dpapi");
byte[] originalAuth = File.ReadAllBytes(authPath);
fixture.NextSession = UnboundSession();
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
using(AgentRuntime runtime = fixture.Runtime())
{
await Assert.ThrowsAsync<QqqSessionException>(() => runtime.SignInAsync(new PipeRequest("sign-in", SessionCredential: fixture.NextSession.ExportCredential()), timeout.Token));
@@ -41,7 +41,7 @@ public sealed class AgentPipeFramingTests
public async Task Write_CancelsWhenConnectedClientDoesNotRead()
{
string name = "qqq-framing-" + Guid.NewGuid().ToString("N");
using CancellationTokenSource deadline = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource deadline = new(TimeSpan.FromSeconds(60));
await using NamedPipeServerStream server = new(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly, 4096, 4096);
await using NamedPipeClientStream client = new(".", name, PipeDirection.InOut, PipeOptions.Asynchronous);
Task connect = client.ConnectAsync(deadline.Token);
@@ -60,7 +60,7 @@ public sealed class AgentPipeFramingTests
public async Task Read_CancelsWhenConnectedClientNeverTerminatesCommand()
{
string name = "qqq-framing-" + Guid.NewGuid().ToString("N");
using CancellationTokenSource deadline = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource deadline = new(TimeSpan.FromSeconds(60));
await using NamedPipeServerStream server = new(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
await using NamedPipeClientStream client = new(".", name, PipeDirection.InOut, PipeOptions.Asynchronous);
Task connect = client.ConnectAsync(deadline.Token);
@@ -31,7 +31,7 @@ public sealed class AgentPipeSecurityTests
security.AddAccessRule(new PipeAccessRule(identity.User!, PipeAccessRights.ReadWrite, AccessControlType.Allow));
await using NamedPipeServerStream server = NamedPipeServerStreamAcl.Create(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte,
PipeOptions.Asynchronous | PipeOptions.FirstPipeInstance, 4096, 4096, security);
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
for(int connection = 0; connection < 2; connection++)
{
Task accepted = server.WaitForConnectionAsync(timeout.Token);
@@ -58,7 +58,7 @@ public sealed class AgentPipeSecurityTests
{
string name = "QqqPrintAgent-untrusted-test-" + Guid.NewGuid().ToString("N");
await using NamedPipeServerStream fake = new(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.FirstPipeInstance);
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
Task connected = fake.WaitForConnectionAsync(timeout.Token);
Task<string> request = AgentPipeClient.SendAsync(new PipeRequest(operation, "https://print.example.com", "synthetic-enrollment-never-send"), name);
await connected;
@@ -39,16 +39,16 @@ public sealed class AgentRealtimeBandwidthTests
{
// Both real runtime lanes must reach their independent body streams before
// timing starts, so serialized admission cannot imitate an aggregate cap.
await Task.WhenAll(first.Entered.Task, second.Entered.Task).WaitAsync(TimeSpan.FromSeconds(5));
await Task.WhenAll(first.Entered.Task, second.Entered.Task).WaitAsync(TimeSpan.FromSeconds(60));
long started = Stopwatch.GetTimestamp();
release.TrySetResult();
await fixture.WaitAsync(() => fixture.Printer.Prints == 2).WaitAsync(TimeSpan.FromSeconds(10));
await fixture.WaitAsync(() => fixture.Printer.Prints == 2).WaitAsync(TimeSpan.FromSeconds(60));
TimeSpan elapsed = Stopwatch.GetElapsedTime(started);
// Together these bodies need four seconds at the shared rate; independent
// per-file throttles would permit both to finish after about two seconds.
Assert.True(elapsed >= TimeSpan.FromSeconds(3.5), "Concurrent documents exceeded the configured aggregate download rate.");
await fixture.WaitAsync(() => fixture.State("first-budget") is "SPOOLED" && fixture.State("second-budget") is "SPOOLED").WaitAsync(TimeSpan.FromSeconds(5));
await fixture.WaitAsync(() => fixture.State("first-budget") is "SPOOLED" && fixture.State("second-budget") is "SPOOLED").WaitAsync(TimeSpan.FromSeconds(60));
Assert.Equal(0, fixture.ForbiddenPolls);
}
finally { release.TrySetResult(); }
@@ -34,7 +34,7 @@ public sealed class AgentRealtimeGenerationTests
{
using var fixture = new SessionFixture();
fixture.SaveSession();
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var replacementEntered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -28,7 +28,7 @@ public sealed class AgentRealtimeOrderingTests
using var slow = new GatedDocument(bytes);
fixture.AddJob("slow", () => slow, length: bytes.Length);
await connection.SendAsync("JOB_AVAILABLE", new { });
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(60));
var observed = new TaskCompletionSource<(bool Connected, string? State, int Prints)>(TaskCreationOptions.RunContinuationsAsynchronously);
fixture.BeforeAcknowledgement = async message =>
{
@@ -48,7 +48,7 @@ public sealed class AgentRealtimeOrderingTests
try
{
await connection.SendAsync("DESIRED_STATE", new Qqq.PrintAgent.Core.Printing.DeviceDesiredState(2, false, "CONNECTED"));
(bool connected, string? state, int prints) = await observed.Task.WaitAsync(TimeSpan.FromSeconds(5));
(bool connected, string? state, int prints) = await observed.Task.WaitAsync(TimeSpan.FromSeconds(60));
Assert.False(connected);
Assert.Equal("RECEIVED", state);
Assert.Equal(0, prints);
@@ -98,7 +98,7 @@ public sealed class AgentRealtimeOrderingTests
using var slow = new GatedDocument(bytes);
fixture.AddJob("slow", () => slow, length: bytes.Length);
await connection.SendAsync("JOB_AVAILABLE", new { });
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(60));
await connection.SendAsync("SETTINGS", fixture.Settings with { Revision = 2, NativeSubmissionTimeoutMillis = 2345, MaxPdfBytes = rejectSize ? 1 : fixture.Settings.MaxPdfBytes });
await connection.SendAsync("DESIRED_STATE", new Qqq.PrintAgent.Core.Printing.DeviceDesiredState(2, true, "CONNECTED"));
await fixture.WaitAsync(() => fixture.Runtime.GetSnapshot().Control!.DesiredState.DispatchPaused);
@@ -123,7 +123,7 @@ public sealed class AgentRealtimeOrderingTests
fixture.AddJob("slow", () => slow, length: bytes.Length);
fixture.AddJob("fast", printerId: "other");
await connection.SendAsync("JOB_AVAILABLE", new { });
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(60));
try
{
Assert.Empty(fixture.Printer.Order);
@@ -156,8 +156,8 @@ public sealed class AgentRealtimeOrderingTests
fixture.AddJob("slow", () => slow, length: slowBytes.Length);
fixture.AddJob("fast", () => { fastStarted.TrySetResult(); return new MemoryStream(fastBytes); }, samePrinter ? "printer" : "other", fastBytes.Length);
await connection.SendAsync("JOB_AVAILABLE", new { });
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
await fastStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(60));
await fastStarted.Task.WaitAsync(TimeSpan.FromSeconds(60));
try
{
if(samePrinter)
@@ -24,7 +24,7 @@ public sealed class AgentRealtimeRecoveryTests
byte[] unreadable = [1, 3, 5];
File.WriteAllBytes(path, unreadable);
string name = "qqq-recovery-" + Guid.NewGuid().ToString("N");
using var stop = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var stop = new CancellationTokenSource(TimeSpan.FromSeconds(60));
using var runtime = new AgentRuntime(fixture.DirectoryPath, _ => { }, fixture.Printer, pipeName: name,
pipeFactory: () => new NamedPipeServerStream(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly));
Task running = runtime.RunAsync(stop.Token);
@@ -28,7 +28,7 @@ public sealed class AgentRealtimeTests
using var stalled = new StalledDocument();
fixture.AddJob("large", () => stalled);
await connection.SendAsync("JOB_AVAILABLE", new { });
await stalled.Entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
await stalled.Entered.Task.WaitAsync(TimeSpan.FromSeconds(60));
await connection.SendAsync("DESIRED_STATE", new DeviceDesiredState(2, true, "CONNECTED"));
await fixture.WaitAsync(() => fixture.Runtime.GetSnapshot().Control!.DesiredState.DispatchPaused);
Assert.Equal(0, fixture.Printer.Prints);
@@ -83,7 +83,7 @@ public sealed class AgentRealtimeTests
RealtimeRuntimeFixture.Connection first = await fixture.ConnectAsync(loseSpoolAck: true);
fixture.AddJob("first");
await first.SendAsync("JOB_AVAILABLE", new { });
await first.LostSpoolAck.Task.WaitAsync(TimeSpan.FromSeconds(5));
await first.LostSpoolAck.Task.WaitAsync(TimeSpan.FromSeconds(60));
fixture.AddJob("buffered");
_ = await fixture.ConnectAsync();
await fixture.WaitAsync(() => string.Equals(fixture.State("buffered"), "SPOOLED", StringComparison.Ordinal));
@@ -18,7 +18,7 @@ public sealed class AgentRealtimeTransitionTests
await using var fixture = new RealtimeRuntimeFixture(settings: new RealtimeSettings(Revision: 5));
_ = await fixture.ConnectAsync();
fixture.Settings = fixture.Settings with { Revision = 0 };
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
_ = await fixture.Runtime.EnrollWithGatesAsync(new PipeRequest("enroll", fixture.Peer.Origin.AbsoluteUri, "synthetic-code"), timeout.Token);
_ = await fixture.ConnectAsync();
Assert.Equal(0, fixture.LastHelloSettingsRevision);
@@ -44,11 +44,11 @@ public sealed class AgentRuntimeBacklogTests
{
// Synthetic responses otherwise let RunAsync drain synchronously before this
// assertion starts. Exclude setup/control readiness from the drain budget.
await state.FirstClaim.Task.WaitAsync(TimeSpan.FromSeconds(10));
stop.CancelAfter(TimeSpan.FromSeconds(15));
await state.FirstClaim.Task.WaitAsync(TimeSpan.FromSeconds(60));
stop.CancelAfter(TimeSpan.FromSeconds(90));
state.ReleaseClaims.SetResult();
// Four claims must finish before the configured 20-second idle interval.
await state.Empty.Task.WaitAsync(TimeSpan.FromSeconds(10), stop.Token);
await state.Empty.Task.WaitAsync(TimeSpan.FromSeconds(60), stop.Token);
Assert.Equal(3, printer.Printed);
await Task.Delay(250, stop.Token);
Assert.Equal(4, Volatile.Read(ref state.Claims));
@@ -27,7 +27,7 @@ public sealed class AgentRuntimeHeartbeatTests
{
string directory = Path.Combine(Path.GetTempPath(), "qqq-heartbeat-" + Guid.NewGuid().ToString("N"));
string pipeName = "QqqHeartbeat-test-" + Guid.NewGuid().ToString("N");
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(60));
BlockingPrinter printer = new();
int heartbeats = 0;
Task? running = null;
@@ -74,7 +74,7 @@ public sealed class AgentRuntimePipeResponseTests
public async Task ResponseWait_RespectsDeadline_WhenClientNeverReads()
{
string name = "QqqPrintAgent-test-response-deadline-" + Guid.NewGuid().ToString("N");
using CancellationTokenSource deadline = new(TimeSpan.FromSeconds(5));
using CancellationTokenSource deadline = new(TimeSpan.FromSeconds(60));
await using NamedPipeServerStream server = new(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte,
PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly, 4096, 4096);
await using NamedPipeClientStream client = new(".", name, PipeDirection.InOut, PipeOptions.Asynchronous);
@@ -152,7 +152,7 @@ public sealed class AgentRuntimePipeResponseTests
private readonly string _directory = Path.Combine(Path.GetTempPath(), "qqq-pipe-response-" + Guid.NewGuid().ToString("N"));
private readonly AgentRuntime _runtime;
public string PipeName { get; } = "QqqPrintAgent-test-response-" + Guid.NewGuid().ToString("N");
public CancellationTokenSource Stop { get; } = new(TimeSpan.FromSeconds(15));
public CancellationTokenSource Stop { get; } = new(TimeSpan.FromSeconds(60));
public Task Running { get; }
@@ -26,7 +26,7 @@ public sealed class AgentRuntimeQueuedJobsTests
{
string directory = Path.Combine(Path.GetTempPath(), "qqq-queued-" + Guid.NewGuid().ToString("N"));
string pipeName = "QqqQueued-test-" + Guid.NewGuid().ToString("N");
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(60));
ServerState server = new();
NoPrintBackend printers = new();
AgentRuntime? runtime = null;
@@ -28,7 +28,7 @@ public sealed class AgentRuntimeTests
{
string directory = Path.Combine(Path.GetTempPath(), "qqq-runtime-" + Guid.NewGuid().ToString("N"));
string pipeName = "QqqPrintAgent-test-" + Guid.NewGuid().ToString("N");
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(20));
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(60));
TaskCompletionSource failed = new(TaskCreationOptions.RunContinuationsAsynchronously);
TaskCompletionSource recovered = new(TaskCreationOptions.RunContinuationsAsynchronously);
int requests = 0;
@@ -64,7 +64,7 @@ public sealed class AgentSessionIntegrationTests
using AgentRuntime runtime = fixture.Runtime();
QqqSessionCredential invalid = new(new Uri("https://synthetic.invalid/not-an-origin"), "synthetic-cookie");
await Assert.ThrowsAsync<ArgumentException>(() => runtime.SignInAsync(new PipeRequest("sign-in", SessionCredential: invalid), CancellationToken.None));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
_ = await runtime.SignInAsync(new PipeRequest("sign-in", SessionCredential: fixture.Session.ExportCredential()), timeout.Token);
Assert.True(runtime.GetSnapshot().Authentication!.Authorized);
}
@@ -81,7 +81,7 @@ public sealed class AgentSessionIntegrationTests
using SessionFixture fixture = new();
fixture.SaveSession();
fixture.Session.DelayUnauthorizedPoll = true;
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
using AgentRuntime runtime = fixture.Runtime();
Task<int> oldPoll = runtime.PollControlsOnceAsync(timeout.Token);
await fixture.Session.PollEntered.Task.WaitAsync(timeout.Token);
@@ -35,7 +35,7 @@ public sealed class AgentSessionRecoveryTests
byte[] corrupt = ProtectedData.Protect(Encoding.UTF8.GetBytes(json), null, DataProtectionScope.CurrentUser);
File.WriteAllBytes(Path.Combine(fixture.Local.DirectoryPath, "qqq-session.dpapi"), corrupt);
using AgentRuntime runtime = fixture.Runtime();
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
using(AgentStore unreadable = new(fixture.Local.DirectoryPath))
{
Assert.Throws<InvalidDataException>(() => unreadable.LoadAuthentication());
@@ -60,7 +60,7 @@ public sealed class AgentSessionRecoveryTests
byte[] corrupt = [1, 2, 3, 4];
File.WriteAllBytes(Path.Combine(fixture.Local.DirectoryPath, "qqq-session.dpapi"), corrupt);
using AgentRuntime runtime = fixture.Runtime();
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
fixture.Session.DenyBinding = true;
await Assert.ThrowsAsync<Qqq.PrintAgent.Core.Authentication.QqqSessionException>(() => runtime.SignInAsync(new PipeRequest("sign-in", SessionCredential: fixture.Session.ExportCredential()), timeout.Token));
Assert.Equal(corrupt, File.ReadAllBytes(Path.Combine(fixture.Local.DirectoryPath, "qqq-session.dpapi")));
@@ -96,7 +96,7 @@ public sealed class AgentSessionRecoveryTests
using AgentRuntime runtime = fixture.Runtime();
Assert.False(runtime.GetSnapshot().Enrolled);
Assert.Equal(corrupt, File.ReadAllBytes(path));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
_ = await runtime.SignInAsync(new PipeRequest("sign-in", SessionCredential: fixture.Session.ExportCredential()), timeout.Token);
Assert.False(runtime.GetSnapshot().Authentication!.Authorized);
Assert.Equal(corrupt, File.ReadAllBytes(path));
@@ -128,7 +128,7 @@ public sealed class AgentSessionRecoveryTests
string path = Path.Combine(fixture.Local.DirectoryPath, "qqq-session.dpapi");
File.WriteAllBytes(path, corrupt);
using AgentRuntime runtime = fixture.Runtime();
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
Assert.Contains("revoke the previous session in QQQ", await runtime.SignOutAsync(timeout.Token), StringComparison.Ordinal);
Assert.False(File.Exists(path));
Assert.Equal(corrupt, File.ReadAllBytes(Assert.Single(Directory.GetFiles(fixture.Local.DirectoryPath, "qqq-session.recovery-*.dpapi"))));
@@ -53,7 +53,7 @@ public sealed class AgentTransportTests
{
string directory = Path.Combine(Path.GetTempPath(), "qqq-transport-pipe-" + Guid.NewGuid().ToString("N"));
string name = "qqq-transport-" + Guid.NewGuid().ToString("N");
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
var runtime = new AgentRuntime(directory, _ => { }, new ForbiddenPrinters(), pipeName: name,
pipeFactory: () => new NamedPipeServerStream(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly));
Task run = runtime.RunAsync(timeout.Token);
@@ -60,7 +60,7 @@ public sealed class BrowserCallbackTests
{
string name = "qqq-callback-test-" + Guid.NewGuid().ToString("N");
using QqqBrowserCallback inbox = new(name);
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
Task<Uri> received = inbox.ReceiveAsync(timeout.Token);
string callback = QqqBrowserCallback.RedirectUri + "?code=synthetic&state=synthetic";
await QqqBrowserCallback.ForwardAsync(callback, name, Environment.ProcessPath!, timeout.Token);
@@ -75,7 +75,7 @@ public sealed class BrowserCallbackTests
{
string name = "qqq-callback-test-" + Guid.NewGuid().ToString("N");
using QqqBrowserCallback inbox = new(name);
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
Task<Uri> received = inbox.ReceiveAsync(timeout.Token);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => QqqBrowserCallback.ForwardAsync(
QqqBrowserCallback.RedirectUri + "?code=synthetic&state=synthetic", name, "C:\\not-the-installed-companion.exe", timeout.Token));
@@ -47,7 +47,7 @@ public sealed class InstallerPreflightTests
await process.StandardInput.WriteAsync(script);
process.StandardInput.Close();
string error = await process.StandardError.ReadToEndAsync();
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
await process.WaitForExitAsync(timeout.Token);
Assert.True(process.ExitCode == 0, error);
}
@@ -56,7 +56,7 @@ public sealed class PrinterWorkerIsolationTests
},
};
PrintJob job = new("synthetic", "fixture", "Never printed", "PDF", new string('0', 64), 1, new PrintOptions(), "", "RECEIVED");
using var watchdog = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var watchdog = new CancellationTokenSource(TimeSpan.FromSeconds(60));
await Assert.ThrowsAsync<TimeoutException>(() => backend.PrintFileAsync(job, Path.Combine(Path.GetTempPath(), "unused-synthetic.document"), TimeSpan.FromSeconds(1), watchdog.Token));
Assert.True(ProcessExited(childId));
}
@@ -101,7 +101,7 @@ public sealed class PrinterWorkerIsolationTests
ProcessStartInfo start = new(FixtureExecutable) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true };
start.ArgumentList.Add("--launch-parent");
using Process parent = Process.Start(start) ?? throw new InvalidOperationException("Parent fixture launch failed.");
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(15));
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
try
{
string? line = await parent.StandardOutput.ReadLineAsync(timeout.Token);
@@ -63,7 +63,7 @@ public sealed class QqqBrowserCallbackInboxTests
{
string name = "QqqPrintAgent.Auth.Test." + Guid.NewGuid().ToString("N");
using QqqBrowserCallback inbox = new(name);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(20));
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
Task<Uri> receiving = inbox.ReceiveAsync(timeout.Token);
var steps = new List<string>();
await QqqBrowserCallback.ForwardAsync(QqqBrowserCallback.RedirectUri + "?code=test-code&state=test-state", name, Environment.ProcessPath!, timeout.Token, steps.Add);
@@ -116,7 +116,7 @@ internal sealed class RealtimeRuntimeFixture : IAsyncDisposable
{
WebSocket socket;
try
{ socket = await Peer.AcceptAsync(_stopping.Token).WaitAsync(TimeSpan.FromSeconds(5)); }
{ socket = await Peer.AcceptAsync(_stopping.Token).WaitAsync(TimeSpan.FromSeconds(60)); }
catch(TimeoutException exception) { throw new InvalidOperationException(Diagnostic("WebSocket handshake"), exception); }
var connection = new Connection(socket, this, loseSpoolAck, _stopping.Token);
_connections.Add(connection);
@@ -44,7 +44,7 @@ public sealed class ApplicationTrafficTransportTests
Assert.Equal(handler.Downloaded, before.DownloadedBytes);
Assert.True(before.UploadedBytes > 0);
using var api = PrintApiClient.ForSession(session);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
string staging = Path.Combine(Path.GetTempPath(), "qqq-traffic-" + Guid.NewGuid().ToString("N"));
var job = new PrintJob("job", "printer", "Synthetic", "PDF", Convert.ToHexString(SHA256.HashData(document)), document.Length, new PrintOptions(), "", "CLAIMED");
Task<DownloadedDocument> download = api.DownloadFileAsync(job, staging, new RealtimeSettings(), timeout.Token);
@@ -88,7 +88,7 @@ public sealed class ApplicationTrafficTransportTests
public async Task WebSocketCountsSentAndFragmentedUtf8PayloadsWithoutCountingQueuedFramesOrHandshake()
{
using var peer = new LoopbackWebSocketPeer();
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
using var client = new ClientWebSocket();
client.Options.AddSubProtocol("qqq-printing.v1");
Task<WebSocket> accepting = peer.AcceptAsync(timeout.Token);
@@ -661,9 +661,9 @@ public sealed class QqqSessionAuthenticationTests
await client.SignInWithPasswordAsync("user", "password");
await client.BindDeviceAsync(new QqqDeviceCredential("device-1", "proof"));
Task<HttpResponseMessage> printing = client.SendPrintingAsync(HttpMethod.Get, "device/jobs/job-1/content");
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
using HttpResponseMessage control = await client.SendPrintingAsync(HttpMethod.Post, "device/control/poll").WaitAsync(TimeSpan.FromSeconds(5));
await client.SignOutAsync().WaitAsync(TimeSpan.FromSeconds(5));
await entered.Task.WaitAsync(TimeSpan.FromSeconds(60));
using HttpResponseMessage control = await client.SendPrintingAsync(HttpMethod.Post, "device/control/poll").WaitAsync(TimeSpan.FromSeconds(60));
await client.SignOutAsync().WaitAsync(TimeSpan.FromSeconds(60));
pending.SetResult(Response("{}"));
await Assert.ThrowsAsync<QqqSessionException>(async () => { using HttpResponseMessage response = await printing; });
Assert.False(client.IsAuthenticated);
@@ -73,7 +73,7 @@ public sealed class RealtimeTransportTests
public async Task NativeCookieAndDeviceProofUpgradeKeepsReceivingDuringStalledDocument()
{
using var peer = new LoopbackWebSocketPeer();
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
using var body = new BlockingDocument();
using var handler = new NativeHandler(body);
using var session = new QqqSessionClient(peer.Origin, handler, allowLoopbackHttp: true);