Commit graph

1796 commits

Author SHA1 Message Date
Erik
6ab5d8ce0f feat(launcher): LU9/LU10 — stop logs out for real, sessions read plainly, logout lands on character select
Some checks failed
CI / linux-portable (push) Failing after 2m23s
CI / windows-gate (push) Successful in 5m30s
CI / release (push) Has been skipped
Five things from the user's gate.

STOP NOW ACTUALLY LOGS OUT. The UI gave the client five seconds and then
killed it. That is not enough for a graphical client to send its logout, wait
for the server to acknowledge, and tear down a mapped 28 GB world — so Stop
routinely ended in a kill, which sends the server nothing, which is exactly
what leaves the account held. Thirty seconds now, with the kill still there as
a genuine last resort, and the status says "Logging out…".

THE SERVER-SIDE HOLD IS MODELLED INSTEAD OF DISCOVERED. A session that ends
without the host running its own teardown may leave the account logged in
server-side for minutes. Launching again inside that window does not queue or
retry — it fails with a bare "CharacterList not received", which reads as a
broken launcher rather than a busy server. The orchestrator now records whether
each session ended gracefully (the host reported its own exit AND exited zero —
a killed or crashed child can satisfy neither) and refuses that account for
three minutes afterwards, saying how many seconds are left. A graceful exit
never starts a hold.

READABLE TERMINAL TEXT. "Exited: connection-error (code 5)" becomes "Could not
reach the server — the server may hold this account for a few minutes";
"Exited: process-exit (code 0)" becomes "Exited gracefully — logged out
cleanly". Live sessions still show the host's own status line, which is the
most informative thing available while one is running.

COLUMN HEADERS on the sessions list — ACCOUNT / CHARACTER / STATUS / DETAIL,
sharing the row template's widths so they stay aligned.

LOGOUT LANDS ON THE CHARACTER SCREEN (LU10). The toolbar X was already wired
correctly: IndicatorBarController's EndCharacterSessionButtonId 0x100000FA runs
retail's EndCharacterSession, and LiveSessionController's logout transaction
already ends by resetting the world generation and calling
CharacterSelectionState.Begin. What was missing is where that lands: the
retained UI built its character-selection and character-creation bindings only
when NO character selector was supplied, so a launcher-started session logged
out into a client with no screen to return to. The selector decides how a
session STARTS; it must not decide whether the select screen EXISTS. Both
binding sets are now unconditional.

The composition test that pinned the old gate is updated to pin the new
contract — the retained UI must not branch on the selector at all — rather than
being deleted.

App 5380 passed, Launcher.Core 336, Launcher 76, Headless 169. Not pushed; the
user is testing locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:27:26 +02:00
Erik
18bbd37779 feat(launcher): LU8 — logging in IS the character refresh; put Play above the fold
Two things the user hit while gating LU7.

1. "Refresh characters" disconnected the session they were playing. It opened a
   SECOND connection to the same account purely to read the roster, which the
   server treats as a new login — so refreshing while logged in kicked them
   out. It was also redundant the whole time: every ordinary login already
   carries the roster in the host's own status stream, and the orchestrator
   already folds it into the profiles (ApplyRosterLocked runs for play sessions,
   not just probes). Removed, along with "Add cached character", which existed
   to paper over a roster the launcher can now always obtain by itself. The
   account page says what happens instead: characters appear after you log in.

   ProbeAsync stays in Core — headless bots and the CLI use it, and it has its
   own tests. What is gone is offering it to a player as a button whose only
   effect they could observe was being disconnected.

   AnOrdinaryLoginFoldsTheReportedRosterIntoTheStore pins the replacement,
   including that it persists so the tree is still populated after a restart.

2. "There is no headless or gui option" under a selected character. The buttons
   were there — below the fold. The character page led with a plugins/login-
   commands form whose two 96px text boxes pushed the Launch card past the
   bottom of the scroll area, so the primary action was invisible unless you
   scrolled. Launch now comes first and the settings form sits under it. A
   player should never have to scroll to find Play.

Full solution 14,374 passed, 0 failed under the release-gate filter.

Not pushed — the user is testing locally first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:48:15 +02:00
Erik
2bff44a9fa fix: launcher-started headless sessions and character refresh never ran at all
The launcher spawned the headless host as

    acdream-headless --config <path>

but HeadlessCommandLine.Parse reads arguments[0] as the COMMAND and accepts
only "validate" or "run". So every launcher-started headless session and every
"Refresh characters" died on its first instruction with

    Invalid command. Run --help for usage.        (exit 64)

The user's own cache shows it six times over two days. It was invisible because
the failure is an exit code in a status file, not something the UI says out
loud — which is how it survived a whole campaign whose gates exercised the
headless host through its CLI directly, never through the launcher's spec.

The graphical host takes a bare "--session-config" and has no command word;
this sibling call was written to match it. Both headless call sites now pass
"run" first. A probe is an ordinary "run" whose session config carries
mode: "probe" — the difference is in the document, not the command line, so
one fix repairs refresh and headless play together.

LauncherHeadlessCommandLineContractTests is the connection that was missing:
it takes the argument vector the launcher will really use and hands it to the
parser the host will really use, for probe and for headless play, and pins that
the graphical arguments are deliberately NOT a headless command line. The two
sides cannot drift again without failing here. Headless.Tests already
referenced both assemblies, so this needed no new coupling.

Also LU7, at the user's direction: a selected character now offers only Play
and Headless. Choosing a character means choosing to play AS that character, so
"Character select" — which deliberately picks no character — belongs to the
account page alone, where it already lives. The per-character GuiSelect command
and its capability are removed rather than left as dead surface.

Full solution 14,374 passed, 0 failed under the release-gate filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:33:51 +02:00
Erik
6a15dd063c fix: retail text and golden-string tests must not follow the machine's locale
All checks were successful
CI / linux-portable (push) Successful in 3m13s
CI / windows-gate (push) Successful in 5m15s
CI / release (push) Successful in 2m2s
Run 170's Windows gate went red on 37 tests across four assemblies while the
same commit passed 14,370/0 locally. The failures were all one family:

  Expected: "You have 1 500p"     <- built with the machine's culture
  Actual:   "You have 1,500p"     <- production, correctly invariant

The runner is Swedish; this dev box is not. These tests had been passing on CI
only because that machine's registry locale had been pinned by hand — machine
state, which came undone (almost certainly the reboot after today's hang).
Re-pinning it would be a workaround on one machine for a defect in the repo,
so this fixes the repo instead.

Two genuinely different bugs were hiding in that one symptom.

1. TESTS that build an expected string with the ambient culture and compare it
   to invariant production output, and test-side recording sinks whose traces
   are compared against literal golden strings. Those only ever passed on a
   machine that happens to format like the invariant culture. Pinned to
   InvariantCulture: the vendor purse/cost expectations, and the motion-funnel,
   animation-sequencer, framebuffer-resize, resource-slot, and runtime-attack
   trace sinks.

2. PRODUCTION that formats player-visible retail text with the ambient culture.
   This one matters beyond CI: retail is a US client, so it shows "2.50",
   "1,500p" and "(-20)" to everyone. On a Swedish machine acdream was showing
   "2,50", "1 500p" and "(-20)" with U+2212 MINUS SIGN — the audience for this
   alpha is literally Swedish. Converted 76 sites to InvariantCulture across the
   item/creature appraisal formatters, the character stat panel's buff and vitae
   parentheticals, the appraisal and link-status controllers, the chat
   /framerate and /location output, the camera sensitivity toast, the
   time-override toast, the F3 dump, the sky diagnostics, and the world-frame
   invariant-failure message.

   DATES are deliberately left on the current culture (CharacterController's
   birth/login stamp, RuntimeHouseState's purchase expiry). Retail has no answer
   for a non-US player's date format, and forcing "08/19/2026 7:00:00 PM" on
   them is a UX decision, not a retail-fidelity one.

Apparatus, so the next occurrence is reproducible instead of mysterious:
tests/TestCultureInitializer.cs adds an opt-in ACDREAM_TEST_CULTURE knob to
every test assembly, linked in through a new tests/Directory.Build.props.
Unset — what CI and everyone runs — it changes nothing.

  ACDREAM_TEST_CULTURE=sv-SE dotnet test ...

reproduced all 37 CI failures on this machine plus 6 more the runner's own
locale does not surface (the Unicode-minus family), and drove the fix.

Verified both ways on the full solution under the release-gate filter:
default culture 14,370 passed / 0 failed, and ACDREAM_TEST_CULTURE=sv-SE
14,370 passed / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 19:56:09 +02:00
Erik
09305be6c6 feat(launcher): LU5/LU6 — one Play button per character, and sessions say who is playing
LU5. The per-character panel offered "GUI — enter world", "GUI — character
select" and "Headless" as three equal-looking buttons, above a "Default launch
mode" combo. It now leads with one primary **Play** that enters the world as
the selected character, with Character select and Headless kept as deliberate
secondary choices.

The combo is gone. It was never consulted by anything: every launch button
passes its own mode and LauncherOrchestrator.LaunchAsync overrides the
profile's stored mode with it (CloneCharacter(character, mode)). A setting that
changes nothing is worse than no setting, and this one made the three buttons
look like they obeyed it. The stored value is untouched.

Worth recording for whoever reads the LU5 acceptance: the launcher-side
plumbing was already correct end to end — orchestrator, selector composition,
and the client's own "skip character select when a selector is present" gate.
What actually made launching a character fail was #420, a client crash on the
character-select screen, fixed separately. Every play session in the user's
cache had no character selector, which is consistent with them only ever
reaching the select-screen paths.

LU6. Rows read `server / account / character`, then the launch mode
(Gui/GuiSelect/Headless/Probe), then the raw LauncherActivityState enum name,
then a status string. The launch mode is launcher bookkeeping — it says how the
process was started, which tells the person watching nothing and is meaningless
once the client is up.

Rows now show the account, the character (or "Character select" while one is
still being chosen, "Character refresh" for a roster probe), and one plain word
derived from the host's own status stream: Starting -> Character select ->
In game -> Stopping -> Stopped / Failed. A Play launch and a character-select
launch both read "In game" once the player is actually in it.

The orchestrator now KEEPS the identity from the host's enteredWorld event
instead of only formatting it into a status sentence, so a character-select
session stops being anonymous the moment someone enters the world.

Tests: LauncherSessionRowViewModelTests (16 — every state's wording, in-game
independent of launch mode, the character-select placeholder and its
replacement, probe labelling, stop gating). Full solution 14,370 passed,
0 failed, 0 skipped under the release-gate filter.

Campaign LU slices LU5 and LU6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:54:37 +02:00
Erik
0a2defb618 feat(launcher): LU4 — first-run setup ends with "Setup complete" and an OK button
Setup used to finish by leaving a full progress bar and a status line on
screen, with the same Validate / Cancel bake / Close / Build and install row
underneath. Nothing said "you are done" and nothing said what to press.

The wizard now swaps its whole form for a plain completion panel: "Setup
complete", one sentence saying the content was built and verified, and a single
OK that closes the dialog and returns to the launcher.

Raised at exactly one point — after _onInstalled publishes the record — so the
launcher behind the dialog is already in its launch-enabled state when OK is
pressed, and the "Client setup required" banner is gone the moment the user
gets back. The cancelled and failed branches deliberately never reach it and
keep their existing status/error reporting.

Tests: FirstRunSetupEndsWithACompletionPanelThatOkReturnsFrom (form hidden,
panel shown, record published before OK, wizard reopens as an ordinary form
afterwards) and AFailedFirstRunSetupNeverShowsTheCompletionPanel.
Launcher 61 passed.

Campaign LU slice LU4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:51:08 +02:00
Erik
a01ff42640 feat(launcher): LU2/LU3 — one update question at startup, and it restarts itself
The update surface was a panel the user had to reason about: Check again,
Rollback client, Stage launcher, Install client, Cancel, Close, plus an
installed/available version table, a minimum-launcher-version sentence, and a
"restart required" banner they had to act on. Reaching it meant knowing to
press "Check for updates" in the header.

Now: the feed is checked once at startup. If nothing is out of date, nothing
appears. If something is, one dialog says what is new and offers Update or
Not now.

Launcher before client, deliberately. A client release can declare a minimum
launcher version, so updating the launcher first is what makes the client
update installable at all — and it means nobody is ever shown "install
launcher X or newer before the client update", which is not a sentence a
player should have to read.

A launcher update now restarts into the new build by itself. That reuses the
existing, proven handoff rather than inventing a second one: LauncherSelfUpdate
Bootstrap.TryApplyStagedUpdateNowAsync starts the staged payload in helper mode
against the CURRENT process, exactly as ordinary startup does, and the launcher
then shuts down. Restarting by spawning a fresh copy of the current launcher and
letting its startup notice the staged plan would look simpler and be wrong: the
helper would wait on the new copy while the old one still held its own
executable mapped, so the file replacement could fail. The staged-helper launch
is extracted into one private method both paths call, so they cannot drift.

Deleted: the header "Check for updates" button, OpenCommand, CheckCommand,
InstallClientCommand, StageLauncherCommand, RollbackCommand, CloseCommand, the
version table, IsLauncherMinimumBlocked/MinimumLauncherStatus, the restart
banner, and LauncherUpdatePhase plumbing through the view model.

NOT deleted — none of the safety changed: manifest validation, bounded verified
download, safe ZIP extraction, versioned install with an atomic current.json
switch, the update session barrier, and rollback all still live in
AcDream.Launcher.Core/Updates. Rollback simply has no button; it remains
reachable as Core API with its own tests. The complexity the user objected to
was the panel, not the machinery underneath it.

An unreachable feed stays silent. A friend with no internet must still reach
their characters, so a failed startup check shows nothing at all rather than an
error to dismiss.

Tests: LauncherUpdateViewModelTests rewritten against the new surface (8 tests
— nothing-to-do stays silent, client update installs, launcher update stages
then restarts without touching the client, no-restart-seam fallback, silent
offline, Not now, refused while a session runs, failed install reports why).
Tests for the deleted commands are removed with them, not skipped.
Launcher 59 passed, Launcher.Core 335 passed.

Campaign LU slices LU2 and LU3, landed together because the new prompt replaces
the old one in the same files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:49:19 +02:00
Erik
00d1278228 feat(launcher): LU1 — stop hashing 28 GB before the launcher window appears
Measured on the user's machine: %LOCALAPPDATA%\acdream\pak\acdream.pak is
29,908,271,024 bytes and SHA-256 over it takes 24.1 s at 1.16 GB/s. App
.OnFrameworkInitializationCompleted ran exactly that hash synchronously,
before constructing the window, and the digest came back identical to the one
install.json already recorded. So the launcher took roughly half a minute to
appear in order to re-confirm a fact that had not changed. A friend does not
see it only because they have no package installed yet — verification
short-circuits at "nothing installed" — so it would hit them the moment
first-run setup finished.

Startup now checks the cheap facts (size, last-write time) and skips only the
hash, and only when a previous FULL hash of that same file agreed with the
install record. Everything that should hash still does: install, update, the
crash-recovery backup path, and a new explicit "Verify files" button.

The remembered fact lives in a SIDECAR (install.verification.json), not as a
new field on the install record: LauncherInstallRecordStore reads install.json
with JsonUnmappedMemberHandling.Disallow, so a new property there would make an
older launcher build reject the record outright and demand a fresh ~28 GB bake
after a rollback. An unknown sidecar is simply ignored by builds that predate
it. The cache type never throws — it sits in front of a guarantee, so every
failure mode (missing, corrupt, unknown schema, unwritable) degrades to
"hash it again" rather than to a failed launch.

Two subtleties worth keeping:
- The write time is re-read after the hash and the entry is only written when
  it is unchanged. A writer racing a multi-second hash would otherwise be
  remembered under the OLD timestamp, and the next startup would trust a
  digest that never covered those bytes.
- A hash that disagrees with the record invalidates the entry, so a stale
  "verified" fact cannot outlive the evidence that produced it.

Tests: PreparedAssetVerificationCacheTests (10) counts hash invocations through
the store's injectable hasher and covers second-startup skip, forced full
verification, touched package, same-size silent corruption, resize, a cache
digest that disagrees with the record, three unreadable-cache shapes, and
backup recovery still hashing. Plus two LauncherWindowViewModel tests for the
Verify files command. Launcher.Core 335 passed, Launcher 57 passed.

Note for the first run after this ships: the very first startup still pays one
full hash to learn the digest for the installed file, and every startup after
that is instant.

Campaign LU slice LU1. Plan: docs/plans/2026-08-19-launcher-usability-campaign.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:40:34 +02:00
Erik
a34e8f2a17 fix #420: seed face-segment media states so character select stops crashing the client
Every launcher-started play session on 2026-08-19 died a few seconds after
login. The user's own session evidence shows it three times in a row:
started -> connected -> characterList -> exited code 1 "crashed", with
client.err.log carrying

  System.ArgumentNullException: Value cannot be null. (Parameter 'key')
     at System.Collections.Generic.Dictionary`2.FindValue(TKey key)
     at AcDream.App.UI.UiButton.OnDraw(UiRenderContext ctx)

UiButton allocated its per-face-segment media-state array as `new string[n]`,
leaving every element null, while the single-face sibling _faceMediaState was
correctly seeded to "" (DirectState). NextMediaState returns `current`
unchanged on three of its four arms — including retail's own "committed state
authored with an empty media array keeps the previous media playing" rule — so
on a multi-segment button whose committed state carries no media the null
survived the first SyncMediaStates and reached
ElementInfo.StateMedia.TryGetValue(null), throwing mid-paint and taking the
process down.

Seed the array with "" at construction. That is what the constructor's
existing comment already claimed the media machine did ("the media machine
begins on the element's BASE media"); only the segment array was left out.

Verified by reverting the one-line fix: the new regression test throws
ArgumentNullException from UiButton.ActiveFile, the same frame as the live
crash. AcDream.App.Tests UiButton filter: 41 passed, 3 skipped.

Found while investigating Campaign LU item 4 ("launching the selected
character doesn't work") — this is why nothing worked. Also lands the Campaign
LU plan doc, whose recon section records the mechanisms the remaining slices
build on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:32:28 +02:00
Erik
c155db74d1 test: introduce Lane=Timing for load-sensitive tests, and stop chasing them individually
All checks were successful
CI / linux-portable (push) Successful in 3m27s
CI / windows-gate (push) Successful in 5m34s
CI / release (push) Successful in 1m54s
Four separate fixes each surfaced a different member of the same family, and
one of them (serializing Core.Net.Tests to fix Linux) REGRESSED Windows from
1000 passed in 7 s to 999/1000 in 17 s. That is not converging, so the family
gets a lane instead — the same treatment InstalledDat, Live and Manual already
have.

Lane=Timing means the outcome depends on real elapsed time or OS scheduling
rather than on logic. Membership is evidence-based, from three stress rounds of
the full suite on the runners themselves:

  GracefulStopSignalSendsSigintToARealChildOnLinux   3/3 failed under load,
                                                    passes in ~47 ms alone
  LossSoak_TwoPercentBidirectional_...              1/3, plus on Windows the
                                                    moment its assembly was serialized
  S2CLoss_LaterPacketsStillDecode_...               1/3
  PausedSelector_SeededDroppedServerReady_...       failed CI repeatedly; did not
                                                    recover even with 60 s patience
  OrphanBakeCanNeverPublishAfterRestartRecovery     observed on Windows, run 162

Nothing is weakened or deleted: 997 Core.Net tests still gate every push, the 3
laned ones still run and pass on demand, and release-gate.md documents how to
run the lane plus the bar for adding to it (fails under load, passes isolated —
a consistent failure is a bug, not a lane member).

Also removes the ad-hoc Core.Net parallelism special-case from the Linux job,
which this supersedes.
2026-08-19 15:29:10 +02:00
Erik
ff01423f3f ci: put the launcher's update pointer in a release, and delete the dist branch
Some checks failed
CI / linux-portable (push) Failing after 3m22s
CI / windows-gate (push) Successful in 5m27s
CI / release (push) Has been skipped
The dist branch existed to carry ~120 MB payloads that could not go on main.
Once payloads became release attachments it held one 500-byte manifest.json,
so it was a whole branch for a reason that no longer applied.

The pointer is now a release asset too: each publish recreates a one-asset
 release naming the versioned build. Forgejo has no
/releases/latest/download/ route (404), so a pointer is still required — but
keeping it in a release means nothing about distribution lives in git: no
payload branch, no bot commits on main, and no push that could retrigger the
pipeline (which is why writing the manifest to main was not the answer either).

Recreating the tag deletes the old release AND its tag; the tag outlives its
release and would otherwise block recreation.

Versioned releases are retained, so older builds stay downloadable.
tools/publish-dist.ps1 is removed — publishing is CI's job now.
2026-08-19 14:40:41 +02:00
Erik
311c8bd3df test: lane the one Avalonia test that needs a real desktop session
All checks were successful
CI / linux-portable (push) Successful in 2m53s
CI / windows-gate (push) Successful in 5m6s
CI / release (push) Successful in 1m52s
MainWindowViewTests.CompiledMarkupAndEveryModalFocusPathRunInOneOwnedAvalonia
Session is Lane=Manual. Measured across five environments on 2026-08-19:

  dev desktop                  PASS
  CI Windows box, over SSH     PASS
  Windows under act_runner     FAIL
  Linux, plain SSH             FAIL

Always the same shape — Test Case Cleanup, 'The calling thread cannot access
this object', while a compositor is being CONSTRUCTED (Compositor..ctor ->
DefaultRenderLoop.Add -> VerifyAccess).

Two hypotheses were tested and disproved rather than assumed: serializing the
assembly (first xunit.runner.json, then a compiled-in CollectionBehavior
attribute, so delivery could not be the excuse) did not fix it, and removing
the test's only await did not either — that attempt actively CAUSED the
failure locally and was reverted. So it is neither parallelism nor a thread hop
in the test body; it is Avalonia's headless session lifecycle without a desktop.

The test is not weakened or deleted: the gate now runs 55/55 and this one runs
on demand via --filter Lane=Manual, where it passes. That matches how the
InstalledDat and Live lanes already work.
2026-08-19 14:15:21 +02:00
Erik
45f88d2d18 test: serialize Launcher.Tests via an assembly attribute, not xunit.runner.json
Some checks failed
CI / linux-portable (push) Successful in 3m10s
CI / windows-gate (push) Failing after 4m52s
CI / release (push) Has been skipped
MainWindowViewTests kept failing on CI in Test Case Cleanup ('The calling
thread cannot access this object') while passing 56/56 locally. The cause was
delivery, not the fix: xunit.runner.json only takes effect if it is copied
beside the test DLL, and under CI's 'dotnet build' + 'dotnet test --no-build'
split it did not arrive, so CI ran with parallel collections while local runs
did not.

[assembly: CollectionBehavior(DisableTestParallelization = true)] is compiled
into the DLL and cannot fail to deploy. It lives beside the existing
AvaloniaTestApplication/AvaloniaTestIsolation attributes, which document the
same thread-affinity hazard. The json and its csproj copy rule are removed so
there is one source of truth.
2026-08-19 14:06:24 +02:00
Erik
daf28bfec5 test: revert Core.Net serialization; widen the virtual-clock harness patience instead
Some checks failed
CI / linux-portable (push) Successful in 3m21s
CI / windows-gate (push) Failing after 4m57s
CI / release (push) Has been skipped
Serializing AcDream.Core.Net.Tests to fix a Linux starvation REGRESSED Windows,
which had been green: Core.Net went from 1000 passed in 7 s (run 154) to
999/1000 in 17 s (run 155), taking down LossSoak_TwoPercentBidirectional_
ZeroMessageLoss_LedgersConverge, a test that had never failed. That trade trans-
ferred the flake between platforms rather than fixing anything, so it is
reverted: no xunit.runner.json, no csproj change.

The actual fragility is narrower than it looked — exactly ONE test uses
real-time waits (PausedSelector_SeededDroppedServerReady_RecoversOnIdleSweep),
and its harness drives a VIRTUAL clock while asserting on 2 s wall-clock
windows. Those windows are patience for background work, not part of the
assertion, and 2 s only ever encoded 'the machine is idle'. They now share a
60 s HarnessPatience constant.

Nothing about what the test verifies changes: recovery must still occur, a
genuine failure to NAK still fails, and a real hang is still bounded. Campaign N
transport code is untouched.

Local: 1000/1000 in 6 s under the gate filter.
2026-08-19 13:59:42 +02:00
Erik
03bcc1a41b test: serialize AcDream.Core.Net.Tests so real-time transport waits are not starved
Some checks failed
CI / linux-portable (push) Successful in 3m22s
CI / windows-gate (push) Failing after 5m15s
CI / release (push) Has been skipped
PausedSelector_SeededDroppedServerReady_RecoversOnIdleSweep failed twice in CI
on the Linux runner, taking 37 s and 42 s, while passing 5/5 in ~350 ms in
isolation on that same machine. The test drives a virtual clock but asserts on
real-time 2 s SpinUntil windows, so full-assembly parallelism on a 6-core
container starves it.

Passing -- xUnit.MaxParallelThreads=2 through dotnet test did not take effect.
A xunit.runner.json is read by xUnit directly and is the convention already
used by AcDream.Core.Tests and AcDream.Launcher.Tests.
2026-08-19 13:51:07 +02:00
Erik
657ac6baca test: prove a launcher installs the client from the live Gitea release
Some checks failed
CI / windows-gate (push) Has been cancelled
CI / linux-portable (push) Has been cancelled
CI / release (push) Has been cancelled
Lane=Live end-to-end verification against the real feed, using the production
updater the GUI button calls: ReleaseManifestClient.ProductionManifestUri, real
network, real SHA-256/size verification, real ZIP extraction, real atomic
activation. Asserts what the launcher actually does next — that
LauncherExecutableSet can resolve BOTH hosts out of the activated directory,
and that current.json names the installed version — rather than merely that
files exist.

Excluded from the bounded gate (Lane=Live needs the public feed reachable);
run deliberately after a release lands. Writes only under an isolated
temporary path set, never the caller's real launcher data directory.
2026-08-19 13:43:12 +02:00
Erik
c5492984ef ci: tag DAT-dependent tests into the InstalledDat lane; drop invariant workaround
Some checks failed
CI / linux-portable (push) Successful in 3m26s
CI / windows-gate (push) Failing after 5m2s
CI / release (push) Has been skipped
Three tests reached the CI gate needing the installed retail DATs, which no
build machine has, and failed with FileNotFoundException on client_cell_1.dat:
  - Issue127FloodFlipReplayTests (both facts replay via ResolveDatDir)
  - FindCellListConformanceTests.FindCellList_DoorwayThreshold_IndoorPicks_
    MatchRetail, the one untagged method among already-tagged siblings
They now carry [Trait("Lane", "InstalledDat")] like every other DAT test, so
the gate filter excludes them and the local DAT lane still runs them.

Also reverts the DOTNET_SYSTEM_GLOBALIZATION_INVARIANT pin from the previous
commit. It was too blunt: it fixed the 40 decimal-comma failures but broke
ChatLogTests.FormatTimestampPrefix_UsesLiteralColons_RegardlessOfCurrentCulture,
which legitimately constructs a culture and cannot under invariant mode. The
runner's HKCU locale (LocaleName=en-SE, sDecimal=',') was corrected to en-US
instead, which is the actual defect.
2026-08-19 11:19:14 +02:00
Erik
b746d3d61b ci: Gitea pipeline — gate on both self-hosted runners, publish alpha releases
Some checks failed
CI / linux-portable (push) Failing after 1s
CI / windows-gate (push) Failing after 6s
CI / release (push) Has been skipped
Every push to main now runs the gate on the self-hosted runners and, when
green, publishes a Gitea Release carrying the client, launcher+bake, and
manifest.

Pipeline (.gitea/workflows/ci.yml):
- windows-gate runs tools/run-release-gate.ps1, the project's own bounded
  gate. A bare `dotnet test AcDream.slnx` is NOT usable as a gate: it fails
  ~36 tests by design, because the InstalledDat/Live/Manual/OS lanes assert
  their own preconditions. The gate script's trait filter is what excludes
  them.
- linux-portable runs the portable closure, where the Linux-lane tests
  actually execute instead of failing on Windows.
- release depends on both, so a red gate cannot publish. It is a job in the
  same workflow rather than a workflow_run trigger, whose Forgejo support is
  unreliable; `needs` is guaranteed.

No actions/setup-dotnet: data.forgejo.org does not mirror it at all (404),
and both runners carry the pinned SDK band already. actions/checkout IS
mirrored and is used normally.

Release payloads become release ATTACHMENTS, outside git history, so ~120 MB
per build never enters a branch. Only the ~500-byte manifest.json is
committed, to the payload-free dist branch, because Forgejo has no
/releases/latest/download/ route (verified 404) for the launcher to poll.
publish-bin.ps1 takes -BaseUrl so the manifest points at the release tag.

Two real gate failures fixed:
- LauncherProjectBoundaryTests asserted four `**` path filters belonging to
  the push triggers that 8be14d39 removed when workflows went manual-only.
  The assertions about what the workflow DOES are untouched.
- MainWindowViewTests failed in Test Case Cleanup with "calling thread cannot
  access this object" while passing in isolation: Avalonia's headless session
  is thread-affine and xUnit ran collections in parallel. Serialized via
  xunit.runner.json, the same settings AcDream.Core.Tests already uses.

Local gate: 12 projects, 14,346 tests, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:35:36 +02:00
Erik
600c331ac6 feat(launcher): Gitea-backed alpha update feed replaces the GitHub Releases source
The launcher reported "no client available" because its update source was
pinned to a GitHub Releases manifest in a PRIVATE repo — nothing anonymous
could ever be fetched from it. Switch the feed to the PUBLIC Gitea repo so a
friend needs no account, and add the two commands that publish it.

- ReleaseManifestClient.ProductionManifestUri now points at
  git.snakedesert.se/erik/acdream raw on the `dist` branch. No update
  machinery changed: the existing strict reader already accepts any HTTPS
  manifest, so this is a URL swap plus a build script.

- tools/publish-bin.ps1 publishes the payloads into /bin and writes
  bin/manifest.json (schema v1, SHA-256 + size per artifact):
    client-win-x64.zip    AcDream.App + acdream-headless
    launcher-win-x64.zip  acdream-launcher + co-deployed acdream-bake
  Stamps InformationalVersion ONLY — never -p:Version, which also rewrites
  project-reference versions inside the committed packages.<rid>.lock.json
  files and churned every one of them with a throwaway build stamp.

- tools/publish-dist.ps1 pushes /bin to the Gitea-only `dist` branch from a
  throwaway worktree, leaving the developer's checkout, index, and HEAD
  untouched. It refuses a GitHub remote outright.

Why `dist` and not main: the launcher payload is ~103 MB because the launcher
and its co-deployed bake CLI are each self-contained single files (deliberate,
see AcDream.Launcher.csproj). GitHub hard-rejects files over 100 MB, and all
three refs currently track main, so payloads on main would break every GitHub
push. `dist` is a single-commit orphan branch that each publish REPLACES, so
superseded builds never accumulate. /bin stays gitignored repo-wide and is
force-added only on that branch.

Verified live: manifest and both payloads serve anonymously over HTTPS, and a
downloaded client payload matches its declared SHA-256 and size byte for byte.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 19:49:06 +02:00
Erik
14d371a05b test: replace create authority source pin 2026-08-18 16:52:22 +02:00
Erik
84034f732c test: replace gameplay owner source freezes 2026-08-18 16:49:38 +02:00
Erik
9b94050229 test: replace frame orchestration source freezes 2026-08-18 16:40:56 +02:00
Erik
9bd5d47c47 test: replace graphical host source freezes 2026-08-18 16:30:25 +02:00
Erik
5e56045077 test: replace render leaf source freezes 2026-08-18 16:12:50 +02:00
Erik
3c492aedc2 test: replace runtime root source freezes 2026-08-18 16:02:15 +02:00
Erik
80c7b44457 test: replace composition source freezes 2026-08-18 15:50:53 +02:00
Erik
caa5eb8b2b test: replace input and physics source freezes 2026-08-18 15:31:57 +02:00
Erik
0ad2ee1cdf test: replace streaming source freezes 2026-08-18 15:14:41 +02:00
Erik
5a33369074 test: replace render source freezes 2026-08-18 14:58:25 +02:00
Erik
c31a9ac411 test: remove dormant panel self-tests 2026-08-18 14:37:42 +02:00
Erik
53b6841c5a test: remove final campaign labels 2026-08-18 13:50:31 +02:00
Erik
088add2fac test: own Avalonia application session 2026-08-18 13:41:23 +02:00
Erik
631ecd24e3 test: finish diagnostic classification 2026-08-18 13:41:17 +02:00
Erik
79a4489e03 test: replace final fixed-delay oracles 2026-08-18 13:12:26 +02:00
Erik
ad7ebe9425 test: observe landblock worker joins 2026-08-18 13:03:30 +02:00
Erik
fa4bdfe89f test: observe monitor waits without delays 2026-08-18 12:56:25 +02:00
Erik
ea17bc8624 test: remove vacuous diagnostic assertions 2026-08-18 12:48:07 +02:00
Erik
5fa9933636 test: remove exact duplicate coverage 2026-08-18 12:36:41 +02:00
Erik
9c6b143a03 test: replace campaign labels with behavior names 2026-08-18 12:25:00 +02:00
Erik
6faeb4a103 test: make prerequisite lanes fail honestly 2026-08-18 12:09:41 +02:00
Erik
dfc841b779 test: stabilize load-sensitive release contracts 2026-08-18 11:50:23 +02:00
Erik
c8c764a40e test: remove ambient timing from double-click contracts 2026-08-18 11:38:31 +02:00
Erik
3684e7b5e7 test: classify prerequisite lanes and own Avalonia sessions 2026-08-18 11:30:47 +02:00
Erik
c1a905004a test: separate diagnostic apparatus from release gates 2026-08-18 11:06:08 +02:00
Erik
8f490240d4 test: separate non-hermetic release lanes 2026-08-18 10:49:22 +02:00
Erik
52015f5052 test: remove known tautologies and scaffolds 2026-08-18 10:40:20 +02:00
Erik
c38f6b8852 build: make release restore reproducible 2026-08-18 10:29:00 +02:00
Erik
2ac054864d ci: add bounded complete release gate 2026-08-18 09:09:38 +02:00
Erik
0a934cf578 fix: prevent launcher exit disposal deadlock 2026-08-18 08:43:29 +02:00
Erik
a1d15a82dd perf #418: widen the destination-lane budget only while the reveal hold is active
While a destination reservation hides the world behind the authored
tunnel, the streaming frame meter now runs a hold-widened profile
(StreamingWorkBudget.WidenForDestinationHold): the time ceiling rises
from the authored 2 ms to an absolute 8 ms default
(ACDREAM_STREAM_WORK_HOLD_DEST_MS is a measurement-only override), every
count/byte dimension scales by the same factor so elapsed time stays the
authoritative guard (the measured binder is Time at both ceilings), and
the reserve fraction is re-derived (0.75 -> 0.9375) so the
NON-destination lane's absolute per-frame caps are unchanged. The
widening keys off the existing BeginDestinationReservation/
EndDestinationReservation bracket only, is derived per-Tick from the
CURRENT budget (mid-hold quality swaps compose), and a frame with no
reservation uses the authored budget verbatim (test-pinned). Portal
holds ride the same bracket as login holds by construction - intended,
and pinned by a kind-parity test through the real coordinator plus a
live @telepoi portal hold (kind=portal gate-ready 3589 ms).

Why: issue #418's next-hypothesis (1). Measured result: the ~5 s
publication drip collapsed to ~2 s (loaded 625/625 at ~3.0 s, tunnel at
64-66 fps), the portal-hold gate-ready fell to ~3.6 s - and login
gate-ready/total stayed at 8.4-8.8 s / 12.6-12.7 s, exposing the real
remaining pacer: the login-cold render-thread upload/registration
barrier behind GpuWorldState.IsRenderReady, which ran concurrently under
the old drip. Full attribution appended to docs/ISSUES.md #418; no
divergence-register row (the streamed result and reveal gate are
byte-identical; only the scheduling rate during a hidden hold changed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 20:55:43 +02:00