openclaw/test/scripts/docker-e2e-helper-cli.test.ts
Peter Steinberger 07a637979d
test(tooling,tui,ui): remove low-value tests (batch d458) (#182470)
* test(tooling): consolidate q0064 owner-boundary coverage

Remove workflow source inventories and duplicate release input-capture replays. Package Acceptance retains executable phase, profile/soak and focused repo-live coverage; release-workflow-matrix-plan and openclaw-cross-os-release-workflow retain resource selection.

Keep generic credential, immutable checkout, permission-ceiling, artifact-before-load and publication guards. Retain package identity failures, producer failure, local-image rejection, finalizer delegation and explicit publication approval.

Keep the real timing refit-to-planner route and focused runtime rebalance cases instead of repeating the full planner inventory. Consolidate sampler dry-run, retry/profile, docs-only and unchanged-write assertions into its successful-job flow. Shared-image-artifact and openclaw-performance-workflow retain their owner-boundary contracts.

CI-only campaign: plain oxlint, oxfmt and diff checks passed. No tests, builds or coverage runs. Original 4585 lines reduced to 3061 (33.2%); 40% remains unmet to preserve distinct boundary cases.

* test(scripts): deslop q0066 tests

Remove copied locale label inventories, duplicate candidate digest mutations, and same-branch publication admission variants. Retain deep-overlay ambiguity and all-locale route alignment tests, canonical request CLI bytes, and digest tampering for every top-level manifest owner.

Named keepers: docs-sync-publish deep overlay and generated locale route tests; full-release-candidate-contract CLI and manifest digest tests; openclaw-npm-extended-stable-workflow actual publication admission shell. Plugin SDK acknowledgement and payload validation also retain owner proof in plugin-sdk-api-release-evidence.test.ts and npm-prepared-bundle.test.ts.

CI-only campaign: oxfmt, plain oxlint, and diff check passed. No tests or coverage run. Shard 4457 -> 4269 lines (4.2%); below 40% because remaining rollback, recovery, DMG, controller, planner, and provenance cases guard distinct branches or lifecycle/authority behavior.

* test(scripts): reduce q0066 redundant proof layers

Replace copied workflow fragments with generic trusted-checkout and publication invariants. Retain the executable admission shell and one manifest digest-mismatch case. Remove locale label inventories, fixture identity self-comparisons, and DMG internal command-log snapshots while keeping observable output, ownership, bounded retries, and error paths.

Keep planner allocation, selector pricing, serial timing and isolation proof at the planner boundary. Executor worker propagation remains covered by ci-run-node-test-shard.test.ts: builds child env with cache isolation, includes, overlays, and inherited worker ceilings; serializes plans with a group-owned cache while retaining worker sizing. Refitter provenance remains covered by ci-test-timings.test.ts: keeps mixed worker ceilings out of compact weights and keys runtime observations by their inherited ceiling.

CI-only: formatting, plain oxlint and git diff --check passed; no tests or coverage run. Original shard 4457 -> 3915 lines (-12.2%), still below 40%. Existing create-dmg test imports missing shell-fixture.test-support.js; its script exists so the suite was retained, not deleted as a dead subject.

* test(scripts): consolidate q0066 same-branch cases

Retain one shared ratchet lifecycle plus the distinct legal DOM-island path. Shallow checkout behavior has stronger executable coverage in ci-changed-lint.test.ts: executes the generated controller guard against a depth-one merge checkout for %s.

Keep changed-session as the rollback inventory mismatch representative and wrong-consumer-key as the session identity mismatch representative. Remove equivalent metadata/payload and consumer-session variants. Retain one regex rejection and the independent safe-integer rejection in recovery volume validation; ahead owns the plugin compatibility fail-closed ancestry branch.

First-hop routing now binds every mounted archive to its exact receipt hash, while packs distinct self-update targets without changing the candidate artifact owns package transformation semantics. Negative archive member checks stay here because the private negative packer has no sibling proof.

CI-only: oxfmt, plain oxlint and diff check passed; no tests run. Original shard 4457 -> 3880 lines (-12.9%), not the requested 40%. Independent read-only audits found no dead subject or whole-file duplicate; further large deletion would remove distinct authority, provenance, path, AST or lifecycle proof.

* test(qa-lab): deslop q0067 tests

Remove unasserted cron screenshot/report generation, repeated MCP and worker diagnostics, and a fixture response restatement already checked through status --usage output.

Keep terminal-token lexical variants at src/agents/agent-run-terminal-reply.test.ts and src/auto-reply/reply/agent-runner-presentation.test.ts; retain streamed prefix buffering, channel delivery, empty-final recovery and restart persistence here. Retain CRLF and indented boundary cases.

CI-only validation: oxfmt, oxlint and diff checks passed. No tests or coverage run. Shard lines 4312 to 4127 (4.3%); 40% target not safely reachable without deleting distinct authority, recovery or lifecycle proof.

* test(qa-lab): remove redundant q0067 product-proof layers

Retire three composed E2E replays and the Codex route/recovery matrix.
Keep the real doctor -> login -> hook turn -> usage flow, real native
cancellation/process-loss proof, quota paths and Workboard lifecycle.
Keep heartbeat routing because its QA scenario invokes that exact file.

Remaining owner proof for each retired layer:
- MCP: server-methods/board.mcp-admission.test.ts foreign-requester and
  keep rows; board.test.ts fresh explicit-owner leases; mcp-app-standalone
  post-catalog grant revalidation, allowed resources, and rejected views.
- Continuation: operator-run-authority.reconnect.test.ts retained source;
  cron/isolated-agent/run.operator-continuation.test.ts operator and queued
  owner payload edit; cron/session-run.test.ts restored source lifetime;
  cron/service.one-shot-schedule-ownership.test.ts same-job rearm, restart
  and deletion; sessions-patch and sessions-tool.batch for metadata;
  session-event-automation-result for durable shared-session output.
- Idle timeout: model.provider-hooks.timeout.test.ts real HTTP guards;
  run/settled-turn-finalization.test.ts blank-output and original-timeout
  cases; run/terminal-timeout.test.ts unfinished tool timeout;
  cron/isolated-agent/run.meta-error-status.test.ts error propagation.
- Codex routes: model-catalog-decisions.test.ts subscription preference;
  run/model-setup.ownership.test.ts admitted account-only routes;
  codex auth-profile-runtime-contract and auth-bridge tests for executed
  Platform/subscription login selection and missing-profile rejection.
- Missing-profile presentation: agent-lifecycle-terminal.test.ts safe
  auth-failure text; chat-send-dispatch-errors.test.ts bounded persisted
  recovery; server-chat.goal-lifecycle.test.ts sessions.changed failure;
  agent-turn/agent-wait-dedupe.test.ts replayable error publication.

Remove only the deleted MCP file's stale CI timing entry. No production
or shared fixture changes. Static validation: oxfmt, plain oxlint and
 git diff --check pass. No tests/build/coverage run, per CI-only mode.
Original shard: 4312 -> 2554 lines (-40.77%), 16 -> 9 declarations.

* test(scripts): deslop q0070 tests

Remove copied build and focused-lint inventories; retain representative graph factories, dependency pin admission, and the transcript reader regression from e26f43af8f.

Boundary runner numeric parsing remains covered by normalizes concurrency input; process-output UTF-8 and grouped-output cases own buffering and command display. The selected runner profiling integration owns Vitest argument forwarding. tsdown-config.test.ts retains executable worker import and SDK singleton proof; runtime-process-build-graph.test.ts retains standalone isolation proof.

Consolidate current-workflow recovery setup and invocation without dropping authority or late-drift cases. CI-only: oxfmt, plain oxlint, and diff check pass; no tests, builds, or coverage were run. Reduction: 4261 to 4065 test lines; below the requested 40 percent because the remaining cases protect distinct behavior.

* test(scripts): remove duplicate owner proof in q0070

Retire lease dependency-fingerprint replays covered by setup-node-env-dependency-fingerprint.test.ts, wrapper retention/withdrawal replays covered by crabbox-wrapper.test.ts, and bulk advisory HTTP replays covered by pnpm-audit-prod.test.ts. Keep lease invalidation and gate failure propagation at their own boundaries.

Retain current-workflow route admission and late drift; pr-merge-prior-ci-recovery.test.ts owns exact rejection bytes, unchanged head, live admin, and security revalidation. Replace the private selector shell with native entrypoint proof. install-sh.test.ts owns unsafe-profile symlink refusal; preserve Bun install and Gateway re-pin proof.

Remove Docker source-path inventory scanning and build-order source grep, consolidate tsdown graph selectors, and use the actual tsdown types instead of copied declarations. Preserve one generic Node interpreter invariant, transcript-reader regression, all package authentication paths, and all profiling lifecycle proof.

CI-only: formatting, plain oxlint, and diff check pass; no tests, builds, typechecks, or coverage run. Additional reduction 4065 to 3555 lines; cumulative 4261 to 3555 (16.6 percent). Requested 40 percent remains unmet: further audited candidates protect distinct behavior.

* test(scripts): simplify q0070 fixture structure

Share report files, decoded advisory replies, retained workflow setup, and resistant descendant fixture programs. Replace handwritten Git object staging with the existing isolated-index fixture helper, preserving parent-only entries and file modes. Keep both recovery routes and all lifecycle assertions.

Simplify lease caller inputs, preserve large-status proof with a bounded Buffer, and remove no-longer-used workspace fixture inputs. Keep all distinct behavior and add repository identity to task-binding coverage.

CI-only: oxfmt, plain oxlint, and diff check pass; no tests or builds run. This pass removes 96 lines (3555 to 3459). Cumulative reduction from 4261 is 18.8 percent; the requested 40 percent is not met without dropping distinct behavior.

* test(tui): deslop q0069 tests

Share Gateway scenario defaults, SSE message data and repeated model-request waits. Keep request counts, terminal readiness and failure diagnostics intact.

The real-local steering scenario retains fresh-session isolation and now owns the visible adopted-session label assertion from the duplicate fresh-session scenario. Shared Gateway startup already waits for connection; the retained real Gateway auth scenario also checks it.

* test(tui): remove redundant q0069 e2e layers

Keep history ordering and once-only rendering in the real Gateway remembered-session scenario. Exercise CLI initial-message submission in the retained terminal alias case.

Validation diagnostics remain covered by embedded-backend.test.ts (retains the latest validation failure when aborting), chat-abort.test.ts (includes the active run's safe validation diagnostic), embedded-agent-subscribe.handlers.tools.test.ts (exports validation diagnostics only before execution), and tui-event-handlers.test.ts (sanitizes and truncates abort diagnostics on a UTF-16 boundary). Remove the duplicate whole-process loops and their unused HTTP fixture branch.

Gateway usage forwarding remains in tui-command-forwarding.test-support.ts. Compact footer formatting remains in tui-formatters.test.ts. Busy reset rejection remains in tui-command-handlers.test.ts; superseded history in tui-session-actions.test.ts. Task acceptance remains in tui-task-suggestions-pty.e2e.test.ts; followup and new-session PTY flows remain in tui-pty-local.e2e.test.ts.

Remove the unknown-restore false positive: its setup writer rejects unknown before storing it, already covered by tui-last-session.test.ts. Fallback projection remains in tui-event-handlers.test.ts. Remove redundant behavior-evidence summaries while retaining assertions. CI-only validation: plain oxlint, oxfmt, and diff check; no tests run.

* test(tui): consolidate q0069 owner-boundary coverage

Retain real PTY authentication, local history and fresh-session isolation, Gateway history attach, reconnect drafts, and one Gateway followup roundtrip. Move the local new-session label and clean-context assertions into the local history scenario. The terminal alias now supplies a literal bang-prefixed initial message; keep only startup-history failure in the fake initial-message table.

Retired E2E layer owners: session-message-events.test.ts keeps web and TUI subscribers on the same authoritative transcript and delivers committed messages in transcript order. queue.collect.test.ts drains exclusive admissions separately; queue.authority.test.ts admits operator input in FIFO order. tui-session-abort.test.ts reconciles queued runs and ignores late aborts after selection changes. embedded-session-steering.test.ts steers a local user turn through prepared controller admission.

Command/reset coverage remains in tui-command-handlers.test.ts, gateway-chat.test.ts, server.sessions.create.lifecycle.test.ts, and server.sessions.reset-models.test.ts. Whitespace-prefixed bang routing remains in tui.submit-handler.test.ts; subprocess/config mutation ownership remains in tui-local-shell.test.ts and config-cli.test.ts. Completion remains in components/custom-editor.test.ts; session clearing remains in tui-session-actions.test.ts and footer formatting in tui-formatters.test.ts. Preserve process-owned delivery in the explicit-session scenario.

Case-sensitive ownership remains in tui-session-events.test.ts. Final deduplication remains in tui-event-handlers.test.ts; rich-only internal source replies remain in embedded-agent-runner/run/payloads.test.ts. Remembered-session validation, stale generations, input admission, approvals and schema failures remain in tui-session-identity-pty.e2e.test.ts; retain a real Gateway history attach rather than replaying that state machine over another backend.

Reuse canonical SSE and ordered-cleanup helpers; remove retired multi-agent/proxy fixture options, duplicate JSON serialization, redundant cleanup registrations and duplicate seeded SQL. Retained embedded programs are byte-identical. Static-only gates: oxfmt, plain oxlint and diff check pass; no tests or builds run. Shard LOC: 4426 to 2655, a 40.01 percent reduction.

* test(scripts): deslop q0068 tests

Remove repeated cleanup log write/close fault injection: the retained native log owner matrix covers held-close and aggregate error behavior. Keep an adapter open-error case for runCleanupSmokePhase rethrow, plus command and cancellation provenance.

The cleanup summary/failure-index contract remains in docker-all-scheduler.test.ts (writes Docker run artifacts when cleanup smoke fails). Move installer env forwarding from source greps into the existing same-version package subprocess fixture and reuse npm fixture invocation setup.

CI-only validation: plain oxlint, oxfmt, and git diff --check passed. Tests and coverage intentionally not run. Shard reduction is 4444 to 4312 lines (3.0%); the 40% target remains unmet to preserve distinct lifecycle and safety proof.

* test(ui): deslop q0071 tests

Consolidate HTML sandbox lifetime and file fixtures, and preserve exact credential config assertions through a shared shape builder.

Remove the update-status replay covered by updates-active-run.test.ts (keeps executing run status distinct from separate campaign). Keep all other update flows.

Keep every rendered contrast and picker state while removing repeated preference-write checks. Appearance-settings-defaults.e2e.test.ts covers config-backed theme reset and accent writes; theme-selection-defaults.e2e.test.ts covers selection persistence and profile reconciliation.

CI-only campaign: oxfmt, diff-check and plain oxlint pass; no tests or coverage run. Lines 4357 to 4190 (3.8 percent); below the 40 percent target because remaining scenarios protect distinct security, lifecycle, layout and interaction behavior.

* test(ui): consolidate q0071 e2e coverage layers

Remove duplicated menu inventories, placement policy and retry replays, bulk-delete fencing, editor-only credential interactions, manual update error replay, anonymous background persistence, and capture-only scaffolding.

Remaining owner proof: session-menu.test.ts and sidebar-rail.e2e.test.ts; chat-pane-placement-policy/retry.test.ts; sessions-page.scope-race.test.ts and session-organizer-batch-mutations.test.ts; credential-editor.browser.test.tsx and config-field-discard.test.ts; settings-background.test.ts and background-presentation.e2e.test.ts; overlays-update-campaign.test.ts and config-page.test.ts.

Merge deferred script-order assertions into the existing real sandbox flow. Preserve unsaved HTML preview/source retention, sandbox security, cold-load first-frame geometry, direct-manipulation gestures, unknown credential-write reconciliation, and dual-store draft retirement.

Use the E2E harness for contrast context ownership and consolidate repeated fixtures and frame assertions. CI-only: no tests/builds/coverage run. Oxfmt, plain oxlint and diff-check pass. Shard lines 4357 to 3206; 26.4 percent total reduction, below 40 percent because remaining distinct protections lack equivalent coverage.

* test(scripts): deslop q0073 tests

Consolidate Docker CLI fixture and dispatch-input assertions without dropping behavior. Replace the copied live-shard alias inventory with executable union coverage, and remove same-path UNKNOWN-name, QA preparation, and zero-pass count replays. The retained alias-union, o-z-other preparation, and zero-of-three report cases exercise those paths.

* test(scripts): remove duplicate q0073 proof layers

Keep deferred-gate success and correction publication in pr-correction-publication.test.ts (publishes a reviewed correction through push/sync with github gates), and stamp replacement in pr-prepare-gates.test.ts (clears stale proof when github gates replace remote proof). Retain stale-target rejection locally.

Move direct receipt outcomes into the real Mantis finalizer table. Remove copied release-fanout and manifest inventories already checked by executable selection/loading, plus merged diagnostic snapshots duplicated by individual rejection cases. Drop upgrade input variants that take the same branch; retain base tarball, threshold, invalid-version and missing-swap proof.

* test(scripts): consolidate q0068 process fixtures

Port q0068 cleanup onto the campaign lane, preserving newer Playwright browser-selection assertions in consolidated coverage. The first shard commit was already represented at the lane head.

CI-only validation: oxlint exited 0 for both touched files; oxfmt --check and git diff --check passed. Independent review found no actionable P0-P2 issues. Tests and coverage intentionally not run; affected test execution is deferred to landing PR CI.

* test(scripts): deslop q0072 tests

Factor postbuild file and asset fixtures and bridge options. Consolidate overlay setup, remove the copied asset inventory, and prune repeated alias/provenance mutation matrices while retaining each exact shipped shape and each field guard.

The mixed-runtime-import test retains hashed runtime and contract rewriting plus the gateway-shutdown ordinary-chunk regression. Stable alias generation remains covered by the hashed-root-runtime and default-export tests.

CI-only campaign: oxlint, oxfmt and diff checks passed; tests and coverage are deferred to hosted CI. Keep distinct live-updater authority, rollback, process cleanup, lock and signed-bundle recovery paths rather than forcing the 40 percent line target.

* test(scripts): consolidate q0072 owner-boundary coverage

Replace copied asset inventories with the no-scan invariant, share root-selection and parser fixtures, and remove duplicate alias, idempotence, and isolated-graph scenarios. Keep forwarding in the shared-declaration test, default/named aliases in the default-export test, standalone ownership in update-compat-external-bindings, and UI publication in update-compat-ui-publish.

Trim updater build-state projection assertions and exact error prose, share signed-bundle fixtures, and remove the positive repoint command inventory already guarded by rollback ordering. Preserve all updater authority and recovery paths. CI-only: standalone oxlint, oxfmt, and diff checks; no test or coverage runs.

* test(scripts): share q0072 runtime fixture contexts

Reuse real-file build and candidate contexts across postbuild and compatibility cases. Centralize literal alias expectations and preserve all remaining assertions, failure paths, and historical fixtures. No production or shared-support changes.

CI-only validation: standalone oxlint, oxfmt and diff-check pass. No tests or coverage run. This is an additional bounded reduction, not a claim that the requested 40 percent shard target was met.

* test(release): deslop q0074 tests

Consolidate pinned-producer, protected-tag, child-job and manifest fixtures while retaining all 67 declarations and 141 expanded cases. Keep publication recovery, provenance, archive safety and retry-ordering guards intact; the 40% target is not safe within this shard without dropping distinct behavior. Static lint/format only under campaign CI-only policy.

* test(release): remove redundant q0074 proof layers

Keep the failed-sealer reader case; leave SIGTERM/checkpoint ownership to full-release-validation-state.test.ts: writes the admitted execution plan when SIGTERM interrupts a stalled reuse API. Upload immutability remains in full-release-validation-continuation-workflow.test.ts: restores the immutable attempt-one plan instead of rebuilding child identity.

Remove duplicate legacy compatibility assertions covered by the retained real-boundary bridge case, copied child inventory, call-shape assertions, and same-branch witness variants. Retain distinct provenance, archive, and transport guards. Static gates only under CI-only policy; cumulative line cut remains below the 40% target.

* test(release): shrink q0074 transport fixtures

Use the minimal legacy package manifest for CLI routing and one local reply transport for exact gh endpoints. Preserve cached-reader, archive digest/entry, and wrapper-token handoff assertions. Build retry receipts with the existing producer compositor and remove duplicated receipt/type scaffolding without dropping retry rejection cases.

Drop repeated workflow-wrapper scenarios: direct original-publication reader cases retain cache/producer/seal/digest decisions; full-release-validation-state.test.ts owns immutable plan restoration and full-release-validation-continuation-workflow.test.ts owns upload ordering/immutability. Keep failed-resolution as a direct negative reader case. No subjects are obsolete. Static lint/format only; the cumulative line cut still falls short of 40%.

* test(scripts): deslop q0075 tests

Share release repository fixtures and provenance assertions; compact upgrade session fixtures and consent checks without removing distinct boundary cases. All 64 declarations and 207 expanded cases remain. CI-only validation: plain oxlint, oxfmt, and diff check; hosted CI owns execution. Reduction is below the 40% target because unique upgrade-state, artifact-consent, provenance, and release-graph guards are retained.

* test(scripts): reduce q0075 duplicate proof layers

Replace copied survivor mount/catalog inventories with generic invariants. Collapse same-condition cleanup, findings, empty-store and contextual-reference variants. Keep missing consent proof in the retained custom-surface/unverified-npm cases; source provenance rejection also remains in src/plugins/official-external-install-records.test.ts. Keep Tool Search survival proof in upgrade-survivor-frozen-recipe.test.ts, and seed admission in upgrade-survivor-acp-metadata.test.ts.

Keep artifact-only source disposition in the inline ACP cases; remove its redundant base replay and unused shell missing-path wrapper. Minimize observer-only SQLite fixtures and release record/provenance fixtures. Preserve all distinct release graph, transport, migration and authority checks. CI-only: plain oxlint, oxfmt and diff check; no test or coverage execution. Overall 3949 to 3517 lines; below the requested 40 percent rather than deleting unique guards.

* test(scripts): simplify q0075 fixture layers

Keep seed admission and transcript consumption in upgrade-survivor-acp-metadata.test.ts (observes migrated ACP metadata through the survivor command and rejects lost model or identity); retain the independent recent ordered timestamp invariant here instead of copying seed IDs, model fields and paths. Share migrated-state setup, express consent mutations as data, and minimize snapshot responses while retaining every lifecycle mode and request-key proof.

No source/workflow inventories or absent subjects remain to remove. Broader release fixture facade saved no lines after formatting and was discarded. Static gates only: plain oxlint, oxfmt, diff check. No tests/builds/coverage. Overall 3949 to 3414 lines, below40 percent; further safe removals were not identified without dropping distinct guards.

* test(scripts): deslop q0076 tests

Reuse recovery fixtures and compact mobile request setup without deleting distinct cases. Keep full restart trace equality and remove its duplicate key assertion.

The ClawHub human/receipt gate remains covered by release-approval-workflows.test.ts (replaces only the ClawHub human gate after parent receipt verification succeeds). Keep exact recovery write/upload routing and source binding here.

CI-only validation: plain oxlint, oxfmt and diff checks pass. All 95 test declarations retained; 4131 to 4072 shard lines. Further cuts would remove distinct security, release, isolation or lifecycle proof.

* test(scripts): remove duplicate q0076 coverage layers

Drop repeated parser rows covered by bench-model.test.ts, bench-gateway-concurrency.test.ts and bench-gateway-startup.test.ts. Keep real transport, process-group, HTTP header and restart lifecycle boundaries.

Drop npm alpha, selector, retry and timeout replays: keepers are test/npm-publish-plan.test.ts, plugin-npm-registry-readback.test.ts, plugin-npm-publication-readback.test.ts and release-beta-verifier.test.ts. Recovery success and inactive-service cases remain in upgrade-survivor-restart-fixture.test.ts and upgrade-survivor-config-parking.test.ts.

Remove copied workflow input metadata and same-branch isolation variants. CI-only static validation: plain oxlint, oxfmt and diff checks pass; no tests or builds run. Combined shard reduction is 4131 to 3959 lines, below the requested 40% because remaining fixtures protect distinct boundaries.

* test(scripts): shrink q0076 fixtures and source guards

Use minimal protocol schemas while preserving nested and root reference rewriting, shared dependency reconstruction and canonicalization paths. Reuse test/helpers/process-wait.ts waitForDead for benchmark cleanup; preserve the real process-group and runtime-attribution regressions.

Remove copied mobile capability/status inventories, an unused mock registry counter and the ClawHub script-order restatement. release-approval-workflows.test.ts retains the validated-job authority gate; recovery artifact write/upload paths now have one generic agreement assertion. Keep mobile signature, authority expansion and redaction proof, and preserve recovery argument checks as outer observations.

CI-only validation: plain oxlint, oxfmt and diff checks pass. No tests or builds run. Shard totals 4131 to 3860 lines; the 40% target remains unmet without deleting distinct remaining security and lifecycle contracts.

* test(scripts): preserve current recovery fixture on replay

* test: preserve current fixtures after batch CI failures
2026-10-24 09:44:37 +00:00

411 lines
15 KiB
TypeScript

import { spawnSync } from "node:child_process";
import {
chmodSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { parse } from "yaml";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const LIVE_E2E_WORKFLOW = ".github/workflows/openclaw-live-and-e2e-checks-reusable.yml";
const EXACT_TARGET_REF = "1".repeat(40);
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
function writeArtifact(fileName: string, payload: unknown) {
const root = tempDirs.make("openclaw-docker-e2e-helper-");
const file = path.join(root, fileName);
writeFileSync(file, `${JSON.stringify(payload)}\n`, "utf8");
return file;
}
function runHelper(script: string, ...args: Array<string | Record<string, string>>) {
const maybeEnv = args.at(-1);
const env =
maybeEnv && typeof maybeEnv === "object"
? (args.pop() as unknown as Record<string, string>)
: {};
const nodeArgs = script.endsWith(".mts")
? ["--import", "tsx", script, ...(args as string[])]
: [script, ...(args as string[])];
return spawnSync(process.execPath, nodeArgs, {
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
GH_FORCE_TTY: "0",
NO_COLOR: "1",
...env,
},
});
}
function rerun(...args: Array<string | Record<string, string>>) {
return runHelper("scripts/docker-e2e-rerun.mts", ...args);
}
function writeFailures(overrides: Record<string, unknown> = {}) {
return writeArtifact("failures.json", {
lanes: [{ name: "gateway-network", status: 1 }],
status: "failed",
...overrides,
});
}
function expectUsageError(result: ReturnType<typeof runHelper>, message: string) {
expect(result).toMatchObject({ status: 1, stdout: "" });
expect(result.stderr).toContain(message);
expect(result.stderr).not.toContain("Error:");
expect(result.stderr).not.toContain("at file:");
}
function expectSuccess(result: ReturnType<typeof runHelper>) {
expect(result.status, result.stderr).toBe(0);
expect(result.stderr).toBe("");
return result.stdout;
}
function expectText(text: string, included: readonly string[], excluded: readonly string[] = []) {
for (const value of included) {
expect(text).toContain(value);
}
for (const value of excluded) {
expect(text).not.toContain(value);
}
}
function mockGitHub(
root: string,
artifactNames: string[],
download: { nested?: string; mixed?: boolean; collision?: boolean },
runInfo: Record<string, unknown> = {},
) {
const info = {
headBranch: "main",
headSha: "f".repeat(40),
url: "https://example.invalid/run",
workflowName: "Live E2E",
...runInfo,
};
const binDir = path.join(root, "bin");
const ghPath = path.join(binDir, "gh");
mkdirSync(binDir, { recursive: true });
writeFileSync(
ghPath,
`#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const args = process.argv.slice(2);
const download = ${JSON.stringify(download)};
if (args[0] === 'run' && args[1] === 'view') {
console.log(JSON.stringify(${JSON.stringify(info)}));
} else if (args[0] === 'api') {
console.log(JSON.stringify(${JSON.stringify(artifactNames.map((name) => ({ expired: false, name })))}));
} else if (args[0] === 'run' && args[1] === 'download') {
const name = args[args.indexOf('--name') + 1];
const dir = args[args.indexOf('--dir') + 1];
const target = path.join(dir, download.nested ?? '');
fs.mkdirSync(target, { recursive: true });
if (download.collision) {
fs.writeFileSync(path.join(target, 'targeted-plan.json'), '{}', { flag: 'wx' });
}
fs.writeFileSync(path.join(target, 'failures.json'), JSON.stringify({
lanes: [{ name: name.endsWith('-b') ? 'install-e2e' : 'gateway-network', status: 1 }],
ref: (download.mixed ? (name.endsWith('-a') ? 'a' : 'b') : 'd').repeat(40),
status: 'failed',
}));
} else {
console.error('unexpected gh args: ' + args.join(' '));
process.exit(1);
}
`,
"utf8",
);
chmodSync(ghPath, 0o755);
return { PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}` };
}
function rerunGitHub(env: Record<string, string>, ...args: string[]) {
return rerun("12345", "--repo", "openclaw/openclaw", ...args, env);
}
function downloadedDir(stdout: string) {
const match = stdout.match(/^Downloaded: (.+)$/mu);
const dir = match?.[1];
if (!dir) {
throw new Error(`missing downloaded dir in stdout:\n${stdout}`);
}
return dir;
}
function expectDeclaredDispatchInputs(stdout: string): void {
const workflow = parse(readFileSync(LIVE_E2E_WORKFLOW, "utf8")) as {
on?: { workflow_dispatch?: { inputs?: Record<string, unknown> } };
};
const declared = new Set(Object.keys(workflow.on?.workflow_dispatch?.inputs ?? {}));
const commands = stdout.split(/\r?\n/u).filter((line) => line.includes("gh workflow run"));
expect(commands.length).toBeGreaterThan(0);
for (const line of commands) {
const command = line.slice(line.indexOf("gh workflow run"));
const emitted = [...command.matchAll(/(?:^|\s)-f\s+([a-z0-9_]+)=/gu)].flatMap((match) =>
match[1] === undefined ? [] : [match[1]],
);
expect(emitted.length).toBeGreaterThan(0);
for (const input of emitted) {
expect(declared.has(input), `undeclared workflow_dispatch input: ${input}`).toBe(true);
}
}
}
describe("Docker E2E helper CLIs", () => {
it.each([
["docker-e2e.mjs", "node scripts/docker-e2e.mjs github-outputs <plan.json>"],
[
"docker-e2e-timings.mts",
"Usage: node --import tsx scripts/docker-e2e-timings.mts <summary.json|lane-timings.json>",
],
[
"docker-e2e-rerun.mts",
"node --import tsx scripts/docker-e2e-rerun.mts <run-id|summary.json|failures.json>",
],
])("prints %s help without processing inputs", (script, usage) => {
const result = runHelper(`scripts/${script}`, "--help");
expect(result).toMatchObject({ status: 0, stderr: "" });
expect(result.stdout).toContain(usage);
});
it("prints scheduler helper usage errors without a Node stack trace", () => {
expectUsageError(
runHelper("scripts/docker-e2e.mjs"),
"node scripts/docker-e2e.mjs github-outputs <plan.json>",
);
});
it("rejects oversized scheduler helper JSON artifacts without a Node stack trace", () => {
const file = writeArtifact("summary.json", { filler: "x".repeat(128) });
const result = runHelper("scripts/docker-e2e.mjs", "failed-reruns", file, {
OPENCLAW_DOCKER_E2E_JSON_ARTIFACT_MAX_BYTES: "64",
});
expectUsageError(result, "JSON artifact exceeded 64 bytes");
});
it("rejects unknown timings options without treating them as artifact paths", () => {
const result = runHelper("scripts/docker-e2e-timings.mts", "--wat");
expectUsageError(result, "unknown argument: --wat");
expect(result.stderr).toContain(
"Usage: node --import tsx scripts/docker-e2e-timings.mts <summary.json|lane-timings.json>",
);
expect(result.stderr).not.toContain("ENOENT");
});
it("rejects missing timings limits without a Node stack trace", () => {
for (const limit of [undefined, "-h"]) {
const args = ["scripts/docker-e2e-timings.mts", "summary.json", "--limit"];
const result = runHelper(args[0]!, ...args.slice(1), ...(limit === undefined ? [] : [limit]));
expectUsageError(result, "--limit requires a value");
}
});
it("prints local cleanup reruns without synthesizing Docker lane reruns from failures.json", () => {
const result = rerun(
writeFailures({
lanes: [
{
name: "cleanup-smoke",
rerunCommand: "pnpm test:docker:cleanup",
status: 42,
targetable: false,
},
],
ref: "HEAD",
}),
);
expectText(
expectSuccess(result),
[
"Failed Docker E2E entries: cleanup-smoke",
"No targetable failed Docker E2E lanes found.",
"- cleanup-smoke: pnpm test:docker:cleanup",
],
["docker_lanes='cleanup-smoke'"],
);
});
it("ignores artifact-provided GitHub rerun commands", () => {
const file = writeFailures({
lanes: [{ ghWorkflowCommand: "echo poisoned-command", name: "gateway-network", status: 1 }],
});
const result = rerun(file, "--ref", EXACT_TARGET_REF);
expectText(expectSuccess(result), ["docker_lanes='gateway-network'"], ["poisoned-command"]);
});
it("uses the exact artifact target from summary.json instead of the workflow head", () => {
const targetRef = "a".repeat(40);
const workflowHead = "b".repeat(40);
const file = writeArtifact("summary.json", {
github: { selectedSha: targetRef },
failures: [{ name: "gateway-network", status: 1 }],
lanes: [{ name: "gateway-network", status: 1 }],
status: "failed",
});
expectText(
expectSuccess(rerun(file, { GITHUB_SHA: workflowHead })),
[`Ref: ${targetRef}`, `-f ref='${targetRef}'`],
[workflowHead],
);
});
it("lets an explicit target ref override artifact refs", () => {
const artifactRef = "a".repeat(40);
const explicitRef = "c".repeat(40);
const file = writeFailures({
images: { bare: "ghcr.io/openclaw/openclaw-bare:artifact-a" },
ref: artifactRef,
});
const result = rerun(file, "--ref", explicitRef);
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain(`Ref: ${explicitRef}`);
expect(result.stdout).toContain(`-f ref='${explicitRef}'`);
expect(result.stdout).not.toContain(`-f ref='${artifactRef}'`);
expect(result.stdout).not.toContain("docker_e2e_bare_image=");
expect(result.stdout).not.toContain("shared_image_policy=existing-only");
});
it.each([
[undefined, [], ["missing an exact target ref", "pass --ref explicitly"], ["HEAD"]],
["HEAD", [], ["invalid artifact target ref", "full commit SHA"], []],
[undefined, ["--ref", "abc123"], ["exact lowercase 40-character target SHA"], []],
] as const)("rejects invalid target ref %s %j", (ref, args, messages, absent) => {
const result = rerun(writeFailures({ ref }), ...args);
expect(result).toMatchObject({ status: 1, stdout: "" });
expectText(result.stderr, messages, absent);
});
it("preserves declared rerun inputs but ignores package and workflow refs", () => {
const file = writeFailures({
lanes: [
{
ghWorkflowCommand:
"gh workflow run 'openclaw-live-and-e2e-checks-reusable.yml' --ref 'full-release-validation-temp-deleted' -f package_artifact_run_id='12345' -f package_artifact_name='docker-e2e-package' -f docker_e2e_bare_image='ghcr.io/openclaw/openclaw-bare:test' -f published_upgrade_survivor_baselines='openclaw@2026.5.3' -f published_upgrade_survivor_scenarios='plugin-dependency-cleanup' -f allow_unreleased_changelog=true -f unsafe_input='do-not-copy'",
name: "published-upgrade-survivor-openclaw-2026-5-3",
status: 1,
},
],
ref: EXACT_TARGET_REF,
});
const result = rerun(file, "--ref", EXACT_TARGET_REF);
expect(result).toMatchObject({ status: 0, stderr: "" });
const combinedCommand = result.stdout.match(/Combined GitHub rerun:\n([^\n]+)/u)?.[1] ?? "";
const reused = [
"docker_e2e_bare_image='ghcr.io/openclaw/openclaw-bare:test'",
"shared_image_policy=existing-only",
"published_upgrade_survivor_baselines='openclaw@2026.5.3'",
"published_upgrade_survivor_scenarios='plugin-dependency-cleanup'",
];
const ignored = ["package_artifact_run_id=", "package_artifact_name=", "unsafe_input"];
expectText(
combinedCommand,
[...reused, "allow_unreleased_changelog=true"],
[...ignored, "--ref 'full-release-validation-temp-deleted'"],
);
expectText(result.stdout, reused, [...ignored, "do-not-copy"]);
expectDeclaredDispatchInputs(result.stdout);
});
it("downloads GitHub run artifacts into distinct default directories", () => {
const root = tempDirs.make("openclaw-docker-e2e-rerun-gh-");
const generatedDirs: string[] = [];
try {
const env = mockGitHub(
root,
["docker-e2e-gateway-network"],
{ nested: "artifact" },
{ headSha: "abc123" },
);
const first = rerunGitHub(env);
const second = rerunGitHub(env);
expect(first.status, first.stderr).toBe(0);
expect(second.status, second.stderr).toBe(0);
const firstDir = downloadedDir(first.stdout);
const secondDir = downloadedDir(second.stdout);
generatedDirs.push(firstDir, secondDir);
expect(firstDir).not.toBe(secondDir);
for (const [dir, result] of [
[firstDir, first],
[secondDir, second],
] as const) {
expect(path.basename(dir)).toMatch(/^openclaw-docker-e2e-rerun-12345-/u);
const artifactDir = readdirSync(dir, { withFileTypes: true }).find((entry) =>
entry.isDirectory(),
);
expect(artifactDir).toBeDefined();
expect(
existsSync(path.join(dir, artifactDir?.name ?? "", "artifact", "failures.json")),
).toBe(true);
expect(result.stdout).toContain(`-f ref='${"d".repeat(40)}'`);
}
expect(first.stdout).not.toContain("-f ref='abc123'");
} finally {
for (const dir of generatedDirs) {
rmSync(dir, { force: true, recursive: true });
}
}
});
it("isolates files with the same name across downloaded artifacts", () => {
const root = tempDirs.make("openclaw-docker-e2e-rerun-collisions-");
const env = mockGitHub(root, ["docker-e2e-a", "docker-e2e-b"], { collision: true });
const outputDir = path.join(root, "artifacts");
const result = rerunGitHub(env, "--dir", outputDir);
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain("gateway-network");
expect(result.stdout).toContain("install-e2e");
const artifactDirs = readdirSync(outputDir, { withFileTypes: true }).filter((entry) =>
entry.isDirectory(),
);
expect(artifactDirs).toHaveLength(2);
for (const entry of artifactDirs) {
expect(existsSync(path.join(outputDir, entry.name, "targeted-plan.json"))).toBe(true);
}
});
it("fails closed when downloaded artifacts contain mixed target refs", () => {
const root = tempDirs.make("openclaw-docker-e2e-rerun-mixed-refs-");
const env = mockGitHub(root, ["docker-e2e-a", "docker-e2e-b"], {
nested: "nested",
mixed: true,
});
const outputDir = path.join(root, "artifacts");
const result = rerunGitHub(env, "--dir", outputDir);
expect(result).toMatchObject({ status: 1, stdout: "" });
expect(result.stderr).toContain("mixed target refs");
expect(result.stderr).toContain("a".repeat(40));
expect(result.stderr).toContain("b".repeat(40));
const explicitRef = "c".repeat(40);
const override = rerunGitHub(env, "--dir", outputDir, "--ref", explicitRef);
expect(override.status, override.stderr).toBe(0);
expect(override.stdout).toContain(`-f ref='${explicitRef}'`);
});
});