577 lines
28 KiB
Markdown
577 lines
28 KiB
Markdown
# 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 plus Headless `--help` and empty-config `validate` from `headless-portability.yml` | every project/CLI row 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 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 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.
|