Compare commits

...
12 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
node / node (push) Successful in 34s
build / scripts (push) Successful in 34s
windows / windows (push) Successful in 4m54s
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
node / node (push) Successful in 33s
build / scripts (push) Successful in 33s
windows / windows (push) Successful in 5m20s
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
node / node (push) Successful in 34s
build / scripts (push) Successful in 34s
windows / windows (push) Failing after 4m43s
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)
node / node (push) Successful in 34s
build / scripts (push) Successful in 34s
windows / windows (push) Canceled after 15m8s
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
node / node (push) Successful in 32s
build / scripts (push) Successful in 32s
windows / windows (push) Failing after 2m33s
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
node / node (push) Successful in 35s
build / scripts (push) Successful in 35s
windows / windows (push) Failing after 2m10s
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
node / node (push) Successful in 33s
build / scripts (push) Successful in 33s
windows / windows (push) Failing after 2m29s
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)
node / node (push) Successful in 35s
build / scripts (push) Successful in 35s
windows / windows (push) Failing after 4m14s
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
node / node (push) Successful in 40s
build / scripts (push) Successful in 40s
windows / windows (push) Failing after 1m27s
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
node / node (push) Successful in 48s
build / scripts (push) Successful in 48s
windows / windows (push) Failing after 1m3s
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
node / node (push) Successful in 34s
build / scripts (push) Successful in 34s
windows / windows (push) Failing after 1m13s
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
28 changed files with 142 additions and 51 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
+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.' }
@@ -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);