feat(launcher): prepare Campaign LA11 user gate
This commit is contained in:
parent
09d84387a8
commit
f881e5b467
24 changed files with 3538 additions and 58 deletions
571
docs/research/2026-08-14-campaign-la-test-script.md
Normal file
571
docs/research/2026-08-14-campaign-la-test-script.md
Normal file
|
|
@ -0,0 +1,571 @@
|
|||
# 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 A–I 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 A–H. 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 | 5–15 min |
|
||||
| Windows | complete Release solution test, serial | exit 0; ordinary known skips only | 20–60 min |
|
||||
| Windows | focused Launcher.Core update tests and launcher update/startup-option tests | exit 0 | 1–4 min |
|
||||
| Windows | canonical portable project build/test closure from `headless-portability.yml` | every project exits 0 | 10–25 min |
|
||||
| Windows | self-contained single-file launcher publish for `win-x64` and `linux-x64` | launcher + bake roots present, no root DLL fallback | 3–10 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 | 35–90 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 26 commands. It never launches App or Headless in connected mode and
|
||||
never reads a credential.
|
||||
|
||||
### 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: 30 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, and nonabsolute inputs. It writes fixed-timestamp sorted ZIPs,
|
||||
the exact LA10 v1 SHA/size manifest, `fixture-report.json`, a loopback-only
|
||||
server, and an A/B selector. It 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 D–F. 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 A–H
|
||||
|
||||
### 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 30–180 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: 45–200 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: 10–15 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 1–3 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: 5–10 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: 5–10 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: 5–10 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: 5–10 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: 5–10 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: 15–30 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: 60–220 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
|
||||
A–I 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: 3–7 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue