31 KiB
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 containingclient_portal.dat,client_cell_1.dat,client_highres.dat, andclient_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 isolatedlauncher-profiles.json; children receive it through redirected stdin.<OBSERVER_CHARACTER>: a second user-controlled character that can observe a private/tellfrom 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;
guiandguiSelectclient 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:
$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 `
-AllowedOutputRoot $Gate `
-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 32 rows, including the connection-free PID/status/redaction and script-
safety contract suites. -AllowedOutputRoot must be a fresh, explicit
campaign-la-* gate root (or the repository logs root), and output must be a
fresh, empty, non-reparse strict descendant; repository, home, source, payload,
nonempty, and arbitrary existing directories are rejected. The helper 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:
pwsh -NoProfile -File (Join-Path $Repo 'tools/run-campaign-la-preflight.ps1') `
-Repository $Repo `
-AllowedOutputRoot $Gate `
-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: 36 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):
$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), nonabsolute inputs,
output/source overlap in either direction, and any reparse point in source or
output ancestry. It enumerates normalized relative paths with ordinal ordering,
never its own output, and normalizes ZIP origin to Unix on both hosts so
Windows/Linux hashes are identical under multiple cultures while native Linux
extraction retains 0755 for App/Headless/Launcher/Bake and 0644 for ordinary
files. 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:
$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
$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, start this gate-only PID watcher immediately before clicking Refresh/Play. It correlates only the unique isolated session-config path, records neither raw command line nor config contents, and must finish while the child is still live. Its safe sidecar contains the normalized config path, a sanitized command fingerprint, and PID plus an OS-native process-start identity so later PID reuse cannot become a false leak:
$CapturePath = Join-Path $Evidence '<ROW>-process.capture.json'
$CaptureStart = [DateTimeOffset]::UtcNow
$CaptureInfo = [Diagnostics.ProcessStartInfo]::new()
$CaptureInfo.FileName = (Get-Command pwsh).Source
$CaptureInfo.UseShellExecute = $false
$CaptureInfo.CreateNoWindow = $true
foreach ($Value in @(
'-NoProfile', '-File', (Join-Path $Repo 'tools/capture-campaign-la-session-process.ps1'),
'-SessionsDirectory', (Join-Path $WinCache 'launcher/sessions'),
'-CreatedAfterUtc', $CaptureStart.ToString('O'),
'-ReportPath', $CapturePath, '-WaitSeconds', '60')) {
$CaptureInfo.ArgumentList.Add($Value)
}
$CaptureProcess = [Diagnostics.Process]::Start($CaptureInfo)
# Click exactly one Refresh/Play action now, then wait for capture.
$CaptureProcess.WaitForExit()
if ($CaptureProcess.ExitCode) { throw 'Stop: live child PID capture failed.' }
$Capture = Get-Content -LiteralPath $CapturePath -Raw | ConvertFrom-Json
$SessionConfig = Join-Path $WinCache "launcher/sessions/$($Capture.sessionId)/session.json"
$Status = Join-Path $WinCache "launcher/sessions/$($Capture.sessionId)/status.jsonl"
# After Stop and terminal status:
pwsh -NoProfile -File (Join-Path $Repo 'tools/test-campaign-la-session-status.ps1') `
-StatusFile $Status `
-Mode '<probe|guiSelect|gui|headless>' `
-ProcessCapturePath $CapturePath `
-CredentialProfilePath (Join-Path $WinConfig 'launcher-profiles.json') `
-ExpectedSessionId $Capture.sessionId `
-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, exact terminal disconnected.reason == stopped, credential
redaction, and that exact captured process instance is gone. Independent exact
config-path correlation uses Windows CIM or Linux /proc/*/cmdline; it never
globally scans a process name, treats a different start identity on a reused PID
as a different process, and is unaffected by unrelated same-name processes or
Linux's 15-character names. The validator verifies owner-only profile access,
reads only password/secret fields in memory, recursively checks every allowed status
string (including command/error text), and reports only the forbidden-value
count and status hash—never credential content or a credential hash. Keep the
profile, raw session.json/status.jsonl, and raw process-capture sidecar
(which contains the absolute isolated path) local; never upload them.
5. Serial Windows user rows A–H
A — isolated first run, real DAT bake, and release-A client baseline
- Confirm the launcher opens First-run setup and all launch buttons are
unavailable. Save a redacted screenshot as
A-first-run-required.png. - Enter
<ABSOLUTE_RETAIL_DAT_DIRECTORY>in the wizard, select a sensible worker count, and click Validate. Confirm all four DATs pass. - 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. - 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. - Confirm these exact isolated artifacts exist and no
.previous-installremains after success:
$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
- Add
<LA11_TEMP_SERVER>at127.0.0.1:<UNUSED_LOCAL_PORT>, edit its name and port, then remove it. Confirm Cancel/Escape makes no mutation. - Add
<LA11_SERVER>at127.0.0.1:<ACE_PORT>. - 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. - Add
<LA11_ACCOUNT>and enter its real password only in the masked field. - Close and reopen the launcher with the same
$LauncherArguments. Confirm only the real server/account persisted. Save redacted before/reopen images asB-crud-before-reopen.pngandB-crud-after-reopen.png. - Record only the profile file's size/hash, never its contents:
$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
- Select
<LA11_ACCOUNT>, click Refresh characters, and wait for the probe row to finish. Confirm the roster appears without entering world. - Run the validator in
probemode for its session id. Confirm its exact event order isstarted, connected, characterList, disconnected, exited, with noenteredWorld, terminal code 0, and terminal reasonprobe. - In the ACE console/session administration view, confirm the account is no
longer logged in. Save a redacted
C-probe-1-ace-cleared.png. - Repeat steps 1–3 immediately, producing a different session id,
C-probe-2-status.validation.json, andC-probe-2-ace-cleared.png. - Confirm Refresh is re-enabled and no
acdream-headlessprocess 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
- Select one non-disposable roster character. Set its mode to
guiSelect, Plugins to exactlyacdream.smoke, and its one login command to/tell <OBSERVER_CHARACTER>, LA11-D-<UNIQUE_NONSECRET_NONCE>. Save settings. - 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. - Select the configured character and enter world. Confirm the observer gets
the exact D nonce once. Save
D-observer-tell.pngwith names redacted. - Click Stop in the launcher. Confirm the game closes gracefully and ACE
releases the account. Validate
guiSelectwith-ExpectedPlugin acdream.smoke.
Expected time: 5–10 minutes.
E — direct gui, plugin, and login command
- Change the same character to
gui, retainacdream.smoke, and change the command nonce toLA11-E-<UNIQUE_NONSECRET_NONCE>. - 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.
- Stop from the launcher, confirm ACE logout, and validate
guiwith the expected plugin. SaveE-world.png,E-observer-tell.png, andE-status.validation.jsonwith identifying text redacted.
Expected time: 5–10 minutes.
F — headless, plugin/login command, and connected #397 acceptance
- Change the same character to
headless, retainacdream.smoke, and useLA11-F-<UNIQUE_NONSECRET_NONCE>. - Click Headless. Confirm
pluginLoaded(acdream.smoke),enteredWorld, and the observer's single exact F nonce. - Click Stop once. On Windows this must target that child's distinct
process group with
CTRL_BREAK; it must reachdisconnectedthenexited(code:0, reason:graceful)before the timeout, without a hard kill. ACE must release the account immediately and the launcher must stay open. - Validate
headlesswith the expected plugin and saveF-status.validation.jsonplus 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
- Launch
guiSelectfor<DISPOSABLE_CHARACTER>. Do not enter world. - Confirm ordinary selection enables Enter/Delete and disables Restore. Click Delete, inspect the retail confirmation dialog, cancel once, and confirm no state change.
- Delete again and confirm. Verify the wait dialog, greyed/pending-delete
roster state, constant boolean-ish nonzero
secondsGreyedOut, disabled Enter/Delete, and enabled Restore. The UI must display no countdown. SaveG-deleted.png. - Click Restore and confirm the same GUID returns to ordinary state with
Enter/Delete enabled and Restore disabled. Save
G-restored.png. - Close through launcher Stop, confirm graceful terminal status and ACE release. Validate with:
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 `
-ProcessCapturePath (Join-Path $Evidence 'G-process.capture.json') `
-CredentialProfilePath (Join-Path $WinConfig 'launcher-profiles.json') `
-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
- Record release A's
app/current.json. Start one headless session and wait forenteredWorld. - Switch the fixture atomically to B:
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.'
}
- 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. SaveH-active-refusal.png. - Stop the headless session and validate its graceful status. Install client
B. Confirm
app/current.jsonnames B, A is previous, all installed-file hashes verify, and new sessions resolve from the B directory. - 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, andH-pointer-rollback-a.json; they contain no credentials. - 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.
- Confirm profiles, install record, and update state still come from the
isolated roots;
campaign-la-fixture-release.txtbeside the relaunched executable saysrelease=B;launcher-update/pending.jsonis gone; and no transaction backup remains. Check again and confirm launcher B is current. SaveH-self-update-confirmed.pngand 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:
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:
$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:
$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:
- 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. - 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 be600. - Probe twice: run Refresh twice, validate both status streams in
probemode with nativepwsh, using the same pre-action watcher and exact PID, Linux session-config path, and$LinuxConfig/launcher-profiles.json; confirm ACE clears the account after each. - 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.
- Headless: configure
acdream.smokeand/tell <OBSERVER_CHARACTER>, LA11-I-<UNIQUE_NONSECRET_NONCE>, launch, observe the tell, click Stop, and validateheadless+ expected plugin. Native Linux sends SIGINT and must reach graceful terminal status with no process leak. - 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:
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/*-process.capture.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:
- Restore
<DISPOSABLE_CHARACTER>and verify it is ordinary before closing its session. - 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.
- Close each launcher normally, then stop only the fixture-server process created above. Do not kill ACE as a substitute for logout evidence.
- Leave update pointers on B or roll the isolated client back to A through the UI; never edit pointers or journals by hand.
- Remove the real account through the isolated launcher UI. After review,
delete only the explicitly recorded
$WinConfig/$LinuxConfiggate 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. - 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.