acdream/docs/research/2026-08-14-campaign-la-test-script.md

577 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Campaign LA11 — automated preflight and connected user gate
**Status:** implementation checkpoint only. Run this script after the reviewed
LA10/LA11 commits are integrated and the campaign branch is clean. Campaign LA,
the Linux graphical client, and issue #397 remain open until the user records a
verdict for every applicable row below.
This is the single Campaign LA operator script. The automated section is
display-free and connection-free. Rows AI are deliberately manual and serial:
they use real retail DATs, a local ACE server, user-entered credentials, and
visual judgment that automation cannot supply.
## 1. Safety boundary and required inputs
Use placeholders throughout; never paste a password into a terminal, this
document, a screenshot, or a gate report.
- `<ABSOLUTE_REPOSITORY_ROOT>`: a clean Campaign LA worktree at the exact commit
under test.
- `<ABSOLUTE_RETAIL_DAT_DIRECTORY>`: a read-only source containing
`client_portal.dat`, `client_cell_1.dat`, `client_highres.dat`, and
`client_local_English.dat`.
- `<ACE_PORT>`, `<LA11_SERVER>`, and `<LA11_ACCOUNT>`: a local ACE endpoint and
account. Enter the account password only in the launcher's masked Password
field. The launcher intentionally stores it as plaintext in the **isolated**
`launcher-profiles.json`; children receive it through redirected stdin.
- `<OBSERVER_CHARACTER>`: a second user-controlled character that can observe a
private `/tell` from each play mode.
- `<DISPOSABLE_CHARACTER>`: a server-operator-approved disposable character.
Never substitute a primary character. If none exists, provision one with the
local server's normal admin procedure before row G.
- Windows 11 x64, PowerShell 7, .NET 10 SDK, a local ACE server, and a supported
Vulkan Windows machine for rows AH. Ubuntu x64 with PowerShell 7 and a Linux
desktop/WSLg is required for row I. The Avalonia launcher is supported on
Linux; `gui` and `guiSelect` **client** actions must remain disabled with the
Modern Runtime Slice-L explanation.
Close every unrelated `AcDream.App`, `acdream-headless`, and acdream launcher
before starting. Do not run another acdream gate in parallel. All generated
files must stay below one new gate directory; the canonical `%APPDATA%`,
`%LOCALAPPDATA%`, and XDG acdream roots are out of scope.
## 2. Automated preflight — no UI, connection, credential, or bake
In PowerShell 7 on Windows:
```powershell
$Repo = [IO.Path]::GetFullPath('<ABSOLUTE_REPOSITORY_ROOT>')
$Stamp = [DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss')
$Gate = Join-Path $Repo "logs/campaign-la-user-gate-$Stamp"
$Preflight = Join-Path $Gate 'automated-preflight'
New-Item -ItemType Directory -Path $Gate | Out-Null
pwsh -NoProfile -File (Join-Path $Repo 'tools/run-campaign-la-preflight.ps1') `
-Repository $Repo `
-OutputDirectory $Preflight
$Report = Get-Content -LiteralPath (Join-Path $Preflight 'report.json') -Raw |
ConvertFrom-Json
if (-not $Report.success -or $Report.dirty) {
throw 'Stop: automated preflight failed or recorded a dirty worktree.'
}
if ($Report.head -cne (git -C $Repo rev-parse HEAD).Trim()) {
throw 'Stop: preflight HEAD does not equal the current HEAD.'
}
```
The expected matrix is:
| Platform | Automated command group | Required result | Typical time |
|---|---|---|---:|
| Windows | Release `AcDream.slnx` build, `-m:1` | exit 0 | 515 min |
| Windows | complete Release solution test, serial | exit 0; ordinary known skips only | 2060 min |
| Windows | focused Launcher.Core update tests and launcher update/startup-option tests | exit 0 | 14 min |
| Windows | canonical portable project build/test closure plus Headless `--help` and empty-config `validate` from `headless-portability.yml` | every project/CLI row exits 0 | 1025 min |
| Windows | self-contained single-file launcher publish for `win-x64` and `linux-x64` | launcher + bake roots present, no root DLL fallback | 310 min |
| Windows | native launcher `--verify-publish` and bake `--help` with bogus `DOTNET_ROOT*` | both exit 0 | <1 min |
| Ubuntu/WSL | run the same helper natively from the Linux path to the worktree | Linux RID report and every row exit 0 | 3590 min |
`report.json` records the tested HEAD/dirty state, OS/RID, exact commands,
durations, exits, redacted logs, and SHA-256/size inventory. A normal preflight
plans 30 rows. It never launches App or Headless in connected mode and
never reads a credential. Every child starts with all inherited `ACDREAM_*`
variables removed, so a developer shell cannot accidentally enable live,
installed-DAT, fixture-regeneration, or diagnostic gates. Only the optional
row below adds the two named DAT variables back for its three exact tests.
### Optional installed-DAT read-only row
This is not a bake and must not replace row A. Add the switches below only when
the DAT directory may be read by tests:
```powershell
pwsh -NoProfile -File (Join-Path $Repo 'tools/run-campaign-la-preflight.ps1') `
-Repository $Repo `
-OutputDirectory (Join-Path $Gate 'automated-preflight-with-dat') `
-IncludeInstalledDat `
-InstalledDatDirectory '<ABSOLUTE_RETAIL_DAT_DIRECTORY>'
```
The mandatory installed-DAT result is
`CharacterManagementLiveDatTests` with both `ACDREAM_PROBE_LIVE_MOUNT=1` and
`ACDREAM_DAT_DIR` set inside the child environment. The helper reads the TRX
and fails if the test skipped or did anything other than pass. The action-map
and portal-asset probes are additional coverage, never substitutes. Expected
matrix size: 34 rows.
On Ubuntu/WSL, invoke the same script with native `pwsh`, a Linux repository
path, and a Linux output path. Do not treat a Windows-hosted run over
`wsl.exe` as the Linux row.
## 3. Prepare the deterministic local A/B feed
Build distinct, version-stamped payloads so the staged launcher really changes
from A to B. These commands write only below `$Gate` (normal project `obj/bin`
incremental outputs are the already-authorized build outputs):
```powershell
$VersionA = '1.0.1-la11.a'
$VersionB = '1.0.1-la11.b'
$Payloads = Join-Path $Gate 'update-payloads'
$Fixture = Join-Path $Gate 'update-fixture'
function Publish-LaRelease([string]$Version, [string]$Label) {
$ClientWin = Join-Path $Payloads "$Label/client-win-x64"
$LauncherWin = Join-Path $Payloads "$Label/launcher-win-x64"
$ClientLinux = Join-Path $Payloads "$Label/client-linux-x64"
$LauncherLinux = Join-Path $Payloads "$Label/launcher-linux-x64"
dotnet publish (Join-Path $Repo 'src/AcDream.App/AcDream.App.csproj') `
-c Release -r win-x64 --self-contained true -p:Version=$Version `
-o $ClientWin --nologo
if ($LASTEXITCODE) { throw "App win-x64 publish failed: $Label" }
dotnet publish (Join-Path $Repo 'src/AcDream.Headless/AcDream.Headless.csproj') `
-c Release -r win-x64 --self-contained true -p:Version=$Version `
-o $ClientWin --nologo
if ($LASTEXITCODE) { throw "Headless win-x64 publish failed: $Label" }
dotnet publish (Join-Path $Repo 'src/AcDream.Launcher/AcDream.Launcher.csproj') `
-c Release -r win-x64 --self-contained true -p:PublishSingleFile=true `
-p:Version=$Version -o $LauncherWin --nologo
if ($LASTEXITCODE) { throw "Launcher win-x64 publish failed: $Label" }
dotnet publish (Join-Path $Repo 'src/AcDream.App/AcDream.App.csproj') `
-c Release -r linux-x64 --self-contained true -p:Version=$Version `
-o $ClientLinux --nologo
if ($LASTEXITCODE) { throw "App linux-x64 publish failed: $Label" }
dotnet publish (Join-Path $Repo 'src/AcDream.Headless/AcDream.Headless.csproj') `
-c Release -r linux-x64 --self-contained true -p:Version=$Version `
-o $ClientLinux --nologo
if ($LASTEXITCODE) { throw "Headless linux-x64 publish failed: $Label" }
dotnet publish (Join-Path $Repo 'src/AcDream.Launcher/AcDream.Launcher.csproj') `
-c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true `
-p:Version=$Version -o $LauncherLinux --nologo
if ($LASTEXITCODE) { throw "Launcher linux-x64 publish failed: $Label" }
}
Publish-LaRelease $VersionA 'A'
Publish-LaRelease $VersionB 'B'
pwsh -NoProfile -File (Join-Path $Repo 'tools/new-campaign-la-update-fixture.ps1') `
-OutputDirectory $Fixture `
-ClientWinX64DirectoryA (Join-Path $Payloads 'A/client-win-x64') `
-LauncherWinX64DirectoryA (Join-Path $Payloads 'A/launcher-win-x64') `
-ClientLinuxX64DirectoryA (Join-Path $Payloads 'A/client-linux-x64') `
-LauncherLinuxX64DirectoryA (Join-Path $Payloads 'A/launcher-linux-x64') `
-ClientWinX64DirectoryB (Join-Path $Payloads 'B/client-win-x64') `
-LauncherWinX64DirectoryB (Join-Path $Payloads 'B/launcher-win-x64') `
-ClientLinuxX64DirectoryB (Join-Path $Payloads 'B/client-linux-x64') `
-LauncherLinuxX64DirectoryB (Join-Path $Payloads 'B/launcher-linux-x64')
```
The helper rejects nonempty output, invalid or non-monotonic versions, missing
root executables (including the co-deployed Bake CLI), and nonabsolute inputs.
It writes fixed-timestamp sorted ZIPs,
the exact LA10 v1 SHA/size manifest, `fixture-report.json`, a loopback-only
server (with optional bounded `-MaximumRequests` smoke mode), and an atomic A/B
selector. Both generated helpers reject a `-Root` other than their own fixture
directory. The generator does not download or mutate payload sources.
Start the Windows loopback server without a shell or visible helper window:
```powershell
$ServerInfo = [Diagnostics.ProcessStartInfo]::new()
$ServerInfo.FileName = (Get-Command pwsh).Source
$ServerInfo.UseShellExecute = $false
$ServerInfo.CreateNoWindow = $true
foreach ($Value in @(
'-NoProfile', '-File', (Join-Path $Fixture 'serve-fixture.ps1'),
'-Root', $Fixture, '-Port', '43119')) {
$ServerInfo.ArgumentList.Add($Value)
}
$FixtureServer = [Diagnostics.Process]::Start($ServerInfo)
$ManifestUri = 'http://127.0.0.1:43119/manifest.json'
if ((Invoke-RestMethod -Uri $ManifestUri).version -cne $VersionA) {
throw 'Stop: local fixture did not begin on release A.'
}
```
## 4. Windows isolated launcher command and evidence rule
```powershell
$WinRoot = Join-Path $Gate 'windows-roots'
$WinConfig = Join-Path $WinRoot 'config'
$WinData = Join-Path $WinRoot 'data'
$WinCache = Join-Path $WinRoot 'cache'
$Evidence = Join-Path $Gate 'evidence'
New-Item -ItemType Directory -Path $Evidence | Out-Null
$LauncherA = Join-Path $Payloads 'A/launcher-win-x64/acdream-launcher.exe'
$LauncherArguments = @(
'--config-dir', $WinConfig,
'--data-dir', $WinData,
'--cache-dir', $WinCache,
'--update-manifest-uri', $ManifestUri)
& $LauncherA @LauncherArguments
```
All four options are process-local. The three roots are an indivisible set;
the local feed reaches only the updater and is never persisted. A self-update
must preserve the same validated suffix through helper and confirmation
restarts. The launcher, profiles, installer, current-version store, updater,
session composer, and orchestrator must all use this one exact path set.
For every play/probe row, copy the session id shown in the launcher's Sessions
list into `<SESSION_ID>`, then run:
```powershell
$Status = Join-Path $WinCache 'launcher/sessions/<SESSION_ID>/status.jsonl'
pwsh -NoProfile -File (Join-Path $Repo 'tools/test-campaign-la-session-status.ps1') `
-StatusFile $Status `
-Mode '<probe|guiSelect|gui|headless>' `
-ExpectedSessionId '<SESSION_ID>' `
-ReportPath (Join-Path $Evidence '<ROW>-status.validation.json')
```
Add `-ExpectedPlugin acdream.smoke` to rows DF. The validator enforces exact
v1 fields **and property order**, one session id, UTC monotonic timestamps,
mode-specific lifecycle order, exit code 0/reason, no unexpected plugin/login
command failure, credential redaction, and no surviving App/Headless process.
Its report contains event names and a hash, not account, character, command, or
error payloads. Keep raw `session.json`/`status.jsonl` local; never upload them.
## 5. Serial Windows user rows AH
### A — isolated first run, real DAT bake, and release-A client baseline
1. Confirm the launcher opens First-run setup and all launch buttons are
unavailable. Save a redacted screenshot as `A-first-run-required.png`.
2. Enter `<ABSOLUTE_RETAIL_DAT_DIRECTORY>` in the wizard, select a sensible
worker count, and click **Validate**. Confirm all four DATs pass.
3. Click **Build and install**. Do not cancel or close the launcher. The real
bake may take 30180 minutes. Confirm every phase reaches **Completed** and
the status says `Client content installed and verified. Launch is enabled.`
4. Open **Check for updates**. Confirm available release A, click **Install
client**, and wait for `Client update installed and activated.` Do not stage
launcher A; the test launcher already has version A.
5. Confirm these exact isolated artifacts exist and no `.previous-install`
remains after success:
```powershell
$RequiredA = @(
(Join-Path $WinData 'install.json'),
(Join-Path $WinData 'pak/acdream.pak'),
(Join-Path $WinData 'app/current.json'))
foreach ($Path in $RequiredA) {
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Missing $Path" }
}
$RequiredA | ForEach-Object {
$Item = Get-Item -LiteralPath $_
[ordered]@{
name = $Item.Name
size = $Item.Length
sha256 = (Get-FileHash -LiteralPath $_ -Algorithm SHA256).Hash.ToLowerInvariant()
}
} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Evidence 'A-install-hashes.json')
```
Do not copy `install.json` into shared evidence because it records the local DAT
path. Expected time: 45200 minutes including bake.
### B — server/account CRUD entirely through the UI
1. Add `<LA11_TEMP_SERVER>` at `127.0.0.1:<UNUSED_LOCAL_PORT>`, edit its name
and port, then remove it. Confirm Cancel/Escape makes no mutation.
2. Add `<LA11_SERVER>` at `127.0.0.1:<ACE_PORT>`.
3. Under it add `<LA11_TEMP_ACCOUNT>` with a user-invented throwaway field
value, edit its account name/value, then remove it. Do not reuse a real
password for this temporary row.
4. Add `<LA11_ACCOUNT>` and enter its real password only in the masked field.
5. Close and reopen the launcher with the **same** `$LauncherArguments`. Confirm
only the real server/account persisted. Save redacted before/reopen images as
`B-crud-before-reopen.png` and `B-crud-after-reopen.png`.
6. Record only the profile file's size/hash, never its contents:
```powershell
$Profile = Join-Path $WinConfig 'launcher-profiles.json'
$Item = Get-Item -LiteralPath $Profile
[ordered]@{
size = $Item.Length
sha256 = (Get-FileHash -LiteralPath $Profile -Algorithm SHA256).Hash.ToLowerInvariant()
} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Evidence 'B-profile-hash.json')
```
Expected time: 1015 minutes.
### C — live character probe twice, no stale ACE session
1. Select `<LA11_ACCOUNT>`, click **Refresh characters**, and wait for the
probe row to finish. Confirm the roster appears without entering world.
2. Run the validator in `probe` mode for its session id. Confirm its exact
event order is `started, connected, characterList, disconnected, exited`,
with no `enteredWorld`, terminal code 0, and terminal reason `probe`.
3. In the ACE console/session administration view, confirm the account is no
longer logged in. Save a redacted `C-probe-1-ace-cleared.png`.
4. Repeat steps 13 immediately, producing a different session id,
`C-probe-2-status.validation.json`, and `C-probe-2-ace-cleared.png`.
5. Confirm Refresh is re-enabled and no `acdream-headless` process remains.
Expected time: 510 minutes. A stale ACE account or timeout is a gate failure;
do not wait three minutes and call the next attempt a pass.
### D — `guiSelect`, retail character screen, plugin, and login command
1. Select one non-disposable roster character. Set its mode to `guiSelect`,
Plugins to exactly `acdream.smoke`, and its one login command to
`/tell <OBSERVER_CHARACTER>, LA11-D-<UNIQUE_NONSECRET_NONCE>`. Save settings.
2. Click **GUI — character select**. Confirm the flat retail character list,
selection highlight, Enter button, Delete/Restore swap state, dialogs, and
absence of any invented rotating 3D preview. Save redacted
`D-character-select.png`.
3. Select the configured character and enter world. Confirm the observer gets
the exact D nonce once. Save `D-observer-tell.png` with names redacted.
4. Click **Stop** in the launcher. Confirm the game closes gracefully and ACE
releases the account. Validate `guiSelect` with
`-ExpectedPlugin acdream.smoke`.
Expected time: 510 minutes.
### E — direct `gui`, plugin, and login command
1. Change the same character to `gui`, retain `acdream.smoke`, and change the
command nonce to `LA11-E-<UNIQUE_NONSECRET_NONCE>`.
2. Click **GUI — enter world**. Confirm it selects the exact cached character,
reaches the world, loads the plugin once, and the observer gets the E nonce
once.
3. Stop from the launcher, confirm ACE logout, and validate `gui` with the
expected plugin. Save `E-world.png`, `E-observer-tell.png`, and
`E-status.validation.json` with identifying text redacted.
Expected time: 510 minutes.
### F — headless, plugin/login command, and connected #397 acceptance
1. Change the same character to `headless`, retain `acdream.smoke`, and use
`LA11-F-<UNIQUE_NONSECRET_NONCE>`.
2. Click **Headless**. Confirm `pluginLoaded(acdream.smoke)`, `enteredWorld`,
and the observer's single exact F nonce.
3. Click **Stop** once. On Windows this must target that child's distinct
process group with `CTRL_BREAK`; it must reach `disconnected` then
`exited(code:0, reason:graceful)` before the timeout, without a hard kill.
ACE must release the account immediately and the launcher must stay open.
4. Validate `headless` with the expected plugin and save
`F-status.validation.json` plus redacted ACE-clear evidence.
The real automated fixture separately proves complex argv and redirected stdin
survive native `CreateProcessW`, the target receives `CTRL_BREAK`, a sibling
process group receives nothing, exit 0 precedes timeout, and `Kill` is never
called. This connected row proves the actual ACE graceful-logout half. Issue
#397 remains open if either half is missing. Expected time: 510 minutes.
### G — disposable delete and restore
1. Launch `guiSelect` for `<DISPOSABLE_CHARACTER>`. Do not enter world.
2. Confirm ordinary selection enables Enter/Delete and disables Restore. Click
Delete, inspect the retail confirmation dialog, cancel once, and confirm no
state change.
3. Delete again and confirm. Verify the wait dialog, greyed roster row/countdown,
disabled Enter/Delete, and enabled Restore. Save `G-deleted.png`.
4. Click Restore and confirm the same GUID returns to ordinary state with
Enter/Delete enabled and Restore disabled. Save `G-restored.png`.
5. Close through launcher **Stop**, confirm graceful terminal status and ACE
release. Validate with:
```powershell
pwsh -NoProfile -File (Join-Path $Repo 'tools/test-campaign-la-session-status.ps1') `
-StatusFile (Join-Path $WinCache 'launcher/sessions/<SESSION_ID>/status.jsonl') `
-Mode guiSelect `
-ExpectNoEnteredWorld `
-ExpectedSessionId '<SESSION_ID>' `
-ExpectedPlugin acdream.smoke `
-ReportPath (Join-Path $Evidence 'G-status.validation.json')
```
If restore fails, stop the row, preserve evidence, and restore only that
disposable character with the server's normal admin recovery. Never continue
with another character. Expected time: 510 minutes.
### H — local A→B client update, active-session refusal, rollback, self-update
1. Record release A's `app/current.json`. Start one headless session and wait
for `enteredWorld`.
2. Switch the fixture atomically to B:
```powershell
pwsh -NoProfile -File (Join-Path $Fixture 'set-active-release.ps1') `
-Release B -Root $Fixture
if ((Invoke-RestMethod -Uri $ManifestUri).version -cne $VersionB) {
throw 'Stop: fixture did not switch to B.'
}
```
3. Open **Check for updates** and **Check again**. While the session is active,
confirm Install client, Rollback client, and Stage launcher are disabled or
refuse without changing `app/current.json`. Save `H-active-refusal.png`.
4. Stop the headless session and validate its graceful status. Install client
B. Confirm `app/current.json` names B, A is previous, all installed-file
hashes verify, and new sessions resolve from the B directory.
5. Click **Rollback client**. Confirm A becomes current and B becomes previous.
Check again and install B once more, leaving B current. Save sanitized copies
of the three pointer states as `H-pointer-a.json`, `H-pointer-b.json`, and
`H-pointer-rollback-a.json`; they contain no credentials.
6. Click **Stage launcher**. Confirm restart is required, then close the
launcher normally. The copied helper must apply B and restart the launcher
with the same config/data/cache/feed suffix.
7. Confirm profiles, install record, and update state still come from the
isolated roots; `campaign-la-fixture-release.txt` beside the relaunched
executable says `release=B`; `launcher-update/pending.json` is gone; and no
transaction backup remains. Check again and confirm launcher B is current.
Save `H-self-update-confirmed.png` and a hash-only post-state inventory.
Never edit a manifest to force this row and never point the launcher at a
non-loopback HTTP endpoint. Expected time: 1530 minutes.
## 6. Row I — native Ubuntu/WSL launcher, XDG-shaped isolated roots
Stop the Windows launcher and fixture server only after every Windows session
is terminal:
```powershell
if (-not $FixtureServer.HasExited) {
$FixtureServer.Kill()
$FixtureServer.WaitForExit()
}
```
In a native Ubuntu/WSL PowerShell 7 terminal, set Linux paths. The repository
and fixture may be read from a mounted Windows path, but roots must live on the
Linux filesystem. Run the generated server natively so its `127.0.0.1` URLs
cannot escape the Linux environment:
```powershell
$RepoLinux = [IO.Path]::GetFullPath('<ABSOLUTE_LINUX_REPOSITORY_PATH>')
$FixtureLinux = [IO.Path]::GetFullPath('<ABSOLUTE_LINUX_FIXTURE_PATH>')
$PayloadsLinux = [IO.Path]::GetFullPath('<ABSOLUTE_LINUX_PAYLOADS_PATH>')
$LinuxGate = [IO.Path]::GetFullPath('<NEW_ABSOLUTE_LINUX_GATE_ROOT>')
$env:XDG_CONFIG_HOME = Join-Path $LinuxGate 'xdg-config-home'
$env:XDG_DATA_HOME = Join-Path $LinuxGate 'xdg-data-home'
$env:XDG_CACHE_HOME = Join-Path $LinuxGate 'xdg-cache-home'
$LinuxConfig = Join-Path $env:XDG_CONFIG_HOME 'acdream'
$LinuxData = Join-Path $env:XDG_DATA_HOME 'acdream'
$LinuxCache = Join-Path $env:XDG_CACHE_HOME 'acdream'
$LinuxEvidence = Join-Path $LinuxGate 'evidence'
New-Item -ItemType Directory -Path $LinuxEvidence | Out-Null
pwsh -NoProfile -File (Join-Path $FixtureLinux 'set-active-release.ps1') `
-Release A -Root $FixtureLinux
```
Start `serve-fixture.ps1 -Root $FixtureLinux -Port 43119` in a dedicated native
terminal and leave it running. In another terminal:
```powershell
$LauncherLinuxA = Join-Path $PayloadsLinux 'A/launcher-linux-x64/acdream-launcher'
& $LauncherLinuxA `
--config-dir $LinuxConfig `
--data-dir $LinuxData `
--cache-dir $LinuxCache `
--update-manifest-uri 'http://127.0.0.1:43119/manifest.json'
```
Complete this exact serial matrix:
1. **Manual-DAT first run:** enter `<ABSOLUTE_LINUX_RETAIL_DAT_DIRECTORY>`;
auto-detection may be empty by design. Validate, bake to
`$LinuxData/pak/acdream.pak`, verify, then install release-A client.
2. **CRUD:** add/edit/remove a temporary server and account entirely in the
launcher, then add the real Linux-reachable ACE profile. Enter its password
only in the masked field. Restart and confirm persistence. Run
`stat -c '%a' "$LinuxConfig/launcher-profiles.json"`; the exact result must
be `600`.
3. **Probe twice:** run Refresh twice, validate both status streams in `probe`
mode with native `pwsh`, and confirm ACE clears the account after each.
4. **Platform posture:** confirm GUI and GUI-select client buttons are disabled
and show the explicit Modern Runtime Slice-L message. Do not bypass this
disablement and do not claim a Linux graphical-client gate.
5. **Headless:** configure `acdream.smoke` and
`/tell <OBSERVER_CHARACTER>, LA11-I-<UNIQUE_NONSECRET_NONCE>`, launch, observe
the tell, click Stop, and validate `headless` + expected plugin. Native Linux
sends SIGINT and must reach graceful terminal status with no process leak.
6. **Update:** switch the native fixture to B, prove update actions refuse while
a headless session is active, stop it gracefully, install B, rollback to A,
reinstall B, stage launcher B, and close normally. Confirm the relaunched
binary's B marker, preserved explicit roots/feed, cleaned pending journal,
and executable owner bits on App, Headless, Launcher, and Bake.
Copy only redacted screenshots, validation reports, pointer JSON, hashes, and
file-mode results into `$LinuxEvidence`. Keep the Linux profile and raw session
files local. Expected time: 60220 minutes, dominated by the real bake.
## 7. Evidence, redaction, verdict, and cleanup
Expected evidence tree:
```text
logs/campaign-la-user-gate-<timestamp>/
automated-preflight/report.json
automated-preflight/commands/*.log
automated-preflight/publish/{win-x64,linux-x64}/...
update-fixture/fixture-report.json
update-fixture/{A,B}/manifest.json
evidence/A-install-hashes.json
evidence/B-*.png
evidence/C-probe-{1,2}-status.validation.json
evidence/D-*.png + D-status.validation.json
evidence/E-*.png + E-status.validation.json
evidence/F-*.png + F-status.validation.json
evidence/G-*.png + G-status.validation.json
evidence/H-*.png + H-pointer-*.json
evidence/I-*.png + I-status.validation.json + I-modes.txt
verdict.json
```
Before sharing evidence:
- remove or mask account names, character names, DAT paths, hostnames other than
loopback, and server-admin identifiers from screenshots;
- never copy `launcher-profiles.json`, raw session configs/status streams,
stdout/stderr that may contain user text, or environment values;
- search the shareable evidence for the exact user-entered password and any
gate-only sentinel secret; the match count must be zero;
- retain SHA-256 and sizes so local raw artifacts remain auditable.
No additional raw child/plugin diagnostic sink is required: `pluginLoaded`,
the strict terminal status, the observer's redacted tell evidence, and the
automated targeted-signal fixture cover the acceptance questions without
capturing credentials or arbitrary chat.
Create `verdict.json` manually with schema version 1, exact tested HEAD, rows
AI as `pass`, `fail`, or `notApplicable`, a short redacted note per row, and
the user's overall verdict. Row I is not applicable only when no native
Ubuntu/WSL desktop is available; it blocks Campaign LA shipping under the
current Linux requirement, so it cannot be silently omitted.
Cleanup is graceful-first and serial:
1. Restore `<DISPOSABLE_CHARACTER>` and verify it is ordinary before closing
its session.
2. Stop every launcher session once; require a passing validator and ACE-clear
observation. If a child survives the timeout, record the gate failure and
its PID before any emergency termination.
3. Close each launcher normally, then stop only the fixture-server process
created above. Do not kill ACE as a substitute for logout evidence.
4. Leave update pointers on B or roll the **isolated** client back to A through
the UI; never edit pointers or journals by hand.
5. Remove the real account through the isolated launcher UI. After review,
delete only the explicitly recorded `$WinConfig`/`$LinuxConfig` gate roots
that held plaintext passwords, or change the test account password. Do not
recursively delete a computed, empty, canonical, home, repository, or XDG
parent path.
6. Preserve the redacted evidence and reports. The large isolated pak/payload
trees may be removed only after resolving and checking their full paths are
descendants of the recorded gate roots.
Estimated total: 37 hours, primarily the two real DAT bakes and full serial
test suites. A failure stops the current row; restore/stop/collect evidence,
then diagnose before advancing. Do not mark LA11, Campaign LA, or #397 shipped
until the user accepts the complete applicable matrix.