mirror of
https://github.com/openclaw/openclaw.git
synced 2026-11-01 04:58:23 +00:00
* test(qa-lab): deslop q0101 tests Share Responses tool-event fixtures while preserving their completion events and token counts. Trim duplicate streaming assertions and copied OTEL success telemetry from the leak-specific fixture. Keep native approval identity, redaction and restart proof. Generic approval duplication remains covered by src/audit/execution-decision-facts.test.ts (rejects approval duplication before creating the generic table). Successful OTEL signal validation remains in the direct producer execution test. * test(qa-lab): finish q0101 proof consolidation Remove obsolete heartbeat payload and cross-process generation-lock proofs. The respective owners were retired in5b4f83ce86and7410a1a70d. Current behavior remains covered by service.heartbeat-payload, isolated-agent.heartbeat-migration, active-memory index trigger-timeout, and manager-index-generation-lease tests. Keep skill-library wire identity and capped authority checks; revision, bundle and ZIP quota behavior stays in src/skills/library/service.test.ts and skill-library-worker-wire.e2e.test.ts. OTLP socket shutdown stays in otel-test-support.test.ts. Successful trace lifecycle is covered by diagnostics-otel service.otlp-export.test.ts; Code Mode continuation and fixture wire shapes by code-mode.agent-loop.test.ts and mock-openai/server.test.ts. Retain real native approval identity/restart, cross-Gateway credentials, paced stream/persistence and visible-child send boundaries. Remove copied inventories, duplicate assertions and unseeded sentinel checks. Shard test LOC: 2840 to 1704 (40 percent). CI-only: formatting, diff check and plain oxlint; no tests or coverage executed. * test(scripts): deslop q0100 tests Replace copied installer workflow inventories with a generic container timeout invariant. Installer behavior remains covered by install-sh.test.ts and the executable syntax, Fedora and platform-admission cases in website-installer-sync-workflow.test.ts. Use the existing synthetic worktree fixture for an independent expected install plan, retaining source plugin and both asset-phase selection instead of reconstructing expectations with production discovery helpers. CI-only campaign: formatting, plain oxlint and diff checks passed; tests and coverage were not run. Retain distinct signing, release provenance, native repair, timing and process lifecycle cases; reduction is below the requested target. * test(scripts): reduce q0100 duplicate proof layers Replace copied workflow inventories with generic barriers and retain representative installer routing branches. Consolidate ClawHub consent into the retained identity scenario and trim unasserted performance fixture metadata. The release-classifier replay is covered by full-release-validation-state.test.ts (blocks beta/stable decisions on failed Windows Node shards). Publisher-only retries remain covered by full-release-child-reuse.test.ts (carries green workload jobs through publisher-only failed-job retries); retain one producer retry composition. Managed process drainage remains covered by managed-child-process.drain.test.ts, managed-child-process.output.test.ts, and write-cli-startup-metadata.test.ts. Keep the command fixture's unique live Linux process-tree redaction and cancellation proof with a smaller IPC-only fixture. CI-only: no tests, coverage or builds run. Formatting, plain oxlint and diff checks pass. Total shard reduction is 251 of 3157 lines (8.0%); the 40% target remains unmet without removing distinct retained contracts. * test(pr): restore signing mutation fixture The q0100 base imported afterBaselineOutput from a helper that does not export it. Restore the shared output-triggered mutation fixture locally, preserving the original three race probes and their post-output ordering. Verified by source inspection, formatting, plain oxlint and diff checks only; test execution is disabled for this campaign. * test(scripts): trim q0100 duplicate variants and inventories Keep one supported plugin sweep with the reinstall regression fromb812fb7b7b, replacing copied mutation counts with the explicit re-enable assertion. Shared consent behavior remains covered by the kitchen-sink, literal argv, replacement and failure-probe cases; keep one probe failure per logging wrapper and error outcome. Retain one signed-payload mismatch for the single unsigned-byte equality guard; coauthor preservation remains in createSignedContext (50520de188). Remove unused child job timestamps/URLs and repeated CPU accounting already asserted by the primary Solid flush case. CI-only: formatting, plain oxlint and diff checks pass; no tests or coverage run. Independent audits found no safe whole-file deletion preserving all distinct behavior. Including the separate missing-helper repair, total shard LOC is 2893 versus original3157; 40 percent target remains unmet. * test(commands): deslop q0104 tests Share Doctor credential-probe fixtures and assertions while preserving all 11 active-credential cases and both custom-override variants. Remove the duplicate OpenCode warning replay: doctor-config-analysis.test.ts retains active and inactive plugin coverage. Remove prompt-shape replay from the inference E2E: onboard-guided.quickstart.test.ts retains custom setup ordering, telemetry, and first-agent defaults; the E2E retains persisted config, real inference, and all interruption/resume paths. CI-only shard: plain oxlint, oxfmt, and diff checks pass. No tests or coverage run. Test LOC 2942 -> 2642 (10.2%); below the 40% target because the remaining process, migration, authority, and protocol paths are distinct. * test(commands): remove duplicate q0104 e2e layers Remove the guided inference composition replay. Retained owners: onboard-guided.custodian.test.ts covers approved-workspace resume, receipt ordering, configured hatch, and workspace failures; setup-apply.test.ts covers team roster construction and post-commit failures; setup-inference-activate.test.ts and onboard-guided.custom-provider.e2e.test.ts retain inference activation and real provider requests. Remove external-channel replay and its fixtures: channel-setup-fallback.test.ts covers loaded catalog plugins without reinstalling; channel-setup/discovery.test.ts covers installed catalog visibility; channel-setup.status.test.ts covers installed status presentation. Keep Discord owner consent and final-channel eligibility cases. Keep the interactive auth-consumer import and repeat-repair scenario, including padded references. auth-alias-migration.test.ts retains collision-free canonical destination selection with occupied profiles; auth-alias-preservation.test.ts retains account and session preservation through repeated repair. doctor-gateway-daemon-flow.test.ts retains noninteractive restart refusal. Replace repeated credential-probe setup with local/remote fixture builders and remove agent-exec filesystem inventories while retaining the absent agent run-state assertion. Remove only deleted-file entries from source routing and proof inventory. CI-only: no tests or coverage run. Plain oxlint, oxfmt, and diff checks pass. Cumulative shard LOC: 2942 -> 2073 (29.5%); the 40% target remains unmet. Remaining distinct authority, migration, protocol, and process-lifetime scenarios were retained. * test(scripts): deslop q0106 tests Consolidate release-prepush and snapshot fixtures without dropping distinct behavior. Keep gate-plan assertions at the runReleasePrepush boundary and zero-delta proof in the fresh-cut case. Remove duplicate public finalization, classification, and moved-tag cases from release-publish-draft: test/scripts/release-publish-finalize.test.ts retains their identical owner proof. Keep unique draft track and failure propagation cases. * test(scripts): deslop q0106 duplicate proof layers Keep pretag orphan cleanup, CLI failure propagation and per-plugin budgets; managed-child-process.test.ts owns timeout descendant cleanup and forwarded signal exit mapping. plugin-release-pretag-pack-check.test.ts keeps timeout diagnostics. Keep publication pipeline failure proof; linux-app-channel.test.ts owns track finalization and explicit latest intent, release-publish-finalize.test.ts owns public/draft classification, and render-github-release-notes.test.ts owns immutable extended-stable context. Replace copied command/metadata inventories with generic owner and manifest invariants. Keep non-Git callers and corrupt-owner evidence; pr-merge-outcome.test.ts owns linked-checkout outcomes and base drift, and pr-wrappers.test.ts owns mismatched prepare dispatch. Consolidate Android resource checks and same-branch parser variants. * test(scripts): minimize q0106 fixtures Reuse existing trusted PR checkout, JSON writing and zombie-aware process helpers instead of maintaining private copies. Keep child cleanup restricted to PIDs above one. Run Android lookup and locale alignment proof once through the full checker. Minimize unused-resource inputs and the missing declaration entry fixture; preserve all distinct error, publication and authority assertions. Link current release tooling dependencies instead of copying their inventory into historical fixtures. * test(release): deslop q0102 tests Share coordinator contexts, source extraction, catalogs, and tarball fixtures. Remove five duplicate qualification rows: tooling-tag reuse, tooling-SHA rejection, Android warnings, npm registry failure, and retained-run reuse remain exercised by other rows in the same table. Retain distinct release provenance, trusted tooling, approval, artifact identity, and recovery cases. Static count: 3192 to 2950 lines; 156 to 151 expanded cases, 55 declarations unchanged. Below the 40 percent target because unique release-safety behavior remains. Plain oxlint and formatting/diff checks pass; runtime proof deferred to hosted CI under campaign CI-only mode. * test(release): trim q0102 orchestration proof layers Remove source-binding and workflow-input inventories, mock stage inventories, repeated call-shape checks, dead fixture branches, and redundant diagnostic text checks. Consolidate state and provenance setup and replace the state-path VM replay with owner checks plus a default-path coordinator case. Keep executable coordinator rejection cases for changelog-before-dispatch and direct evidence SHA mismatch. The prepared-command round trip remains the input contract; validate-full-release-validation-evidence.test.ts owns publication tag and pinned-source validation, while release-candidate-dispatch.test.ts owns retained dispatch recovery. CI-only static proof: oxfmt, plain oxlint (exit 0), and git diff --check. No test or coverage runs. Shard is 3192 to 2633 lines overall (17.5 percent); the 40 percent target remains unmet because unique release-safety flows remain. * test(release): consolidate q0102 qualification proof Keep Telegram artifact identity at the qualifies Telegram boundary and remove the duplicate tuple probe. Retained-request recovery remains covered by release-candidate-dispatch.test.ts (offers only the canonical recovery command). Preflight report formatting remains covered by release-publish-preflight-interface.test.ts (retains failure and remediation in the table without duplicating a prepared command). Share branch fixtures and rejection assertions while retaining release authority, provenance, lifecycle cleanup and publication blocking checks. CI-only: formatting, plain oxlint and diff-check passed; no tests or coverage run. The file is 2300 lines versus 3192 originally (27.9% reduction), short of the 40% target because remaining cases retain distinct branches. * test(ui): consolidate q0105 e2e proof layers Reduce the original shard from 2798 to 1678 lines (40.03%), retaining 25 expanded cases. Consolidate native cold text-drop and typing handoff; keep the mobile cold shell separately. Reuse split-layout/settings fixtures, combine equivalent geometry predicates, trim fixture content, and replace private unread state inspection with its observable follow-up acknowledgement. Coverage retained outside the shard: app-shell-command-palette-loading.test.ts covers pasted-file custody after dismissal and dropped non-image handoff; session-progress-disclosure.test.ts covers gesture thresholds, escalation and pinning; session-progress-disclosure-controller.test.ts covers remount and gateway/session authority; session-progress-scroll.e2e.test.ts retains native wheel/touch and remount proof; session-details-progress.e2e.test.ts retains Details placement and intent. Keep both short-name reconnect choices here. sidebar-attention.test.ts retains loaded health across view-only remount; sidebar-attention-store.test.ts retains identity replacement; sidebar-attention-cron-events.test.ts retains cron publication. connection-page.test.ts retains lazy-route diagnostics and edited drafts/secret masking through reconnect. config-draft-model.test.ts retains dotted validation paths; settings-save-indicator.test.ts retains rejected-save explanation; settings-config-recovery.e2e.test.ts retains discard/reload recovery. chat-history-observation.test.ts retains Retry through the page owner; chat-pane-history-issuance.test.ts retains reconnect recovery. Keep cached-refresh error, both canonicalization paths and timed skeleton proof here. chat-bubble-activity.test.ts retains tool/working/final rendering; chat-history.inflight.test.ts retains preamble custody; chat-responsive.browser.test.ts retains compact activity geometry. Keep native first-fragment latency and bubble corner geometry here. chat-metadata-store.test.ts retains invalidation-wave coalescing; solid/menu-navigation.browser.test.ts retains Back keyboard focus; chat-composer-overflow.browser.test.ts retains label containment. Keep managed-skill single-line descriptions, reader boundaries, activation requests and session isolation. session-row-tint.node.test.ts and session-menu.test.ts retain color paint and actions; keep keyboard surfaces and authoritative color clearing here. CI-only: oxfmt, plain oxlint and git diff --check passed. No tests, coverage, builds, typechecks, Testbox, or autoreview run. No production/shared-support files changed or files deleted. * test(ui): deslop q0107 tests Remove the duplicate dashboard recovery table: dashboards-page.test.tsx keeps the held-first-read/error-retry contract, and index-list.test.ts keeps same-client suspension recovery. Retain the two-browser-tab demand scenario. Keep real Gateway catalog replacement and draft non-mutation proof; generic compact layout, viewport bounds, filtering and keyboard selection remain in select-picker.browser.test.tsx and select-picker.behavior.browser.test.ts. Consolidate managed-install consent assertions into the Meta continuation scenario; custom endpoint cancellation and model-setup-cancel.e2e.test.ts retain cancellation coverage. route.test.ts covers both firstRun query spellings. Remove unasserted session-source gesture diagnostics. CI-only campaign: plain oxlint, oxfmt and diff checks passed. Runtime tests and coverage not run. Test LOC 2505 to 2255; below the 40 percent target to retain distinct authority, lifecycle and layout scenarios. * test(ui): consolidate q0107 e2e layers Reduce the original shard from 2505 to 1501 lines (40.08 percent), after the first q0107 commit. Preserve authority changes, first-turn custody, worker completion, and delayed-wheel renderer regression. CI-only: no tests or coverage run; plain oxlint, oxfmt, and diff checks pass. Dashboard demand is owned by index-list.test.ts (keeps an observed active query independent; retains an in-flight managed query while its route subscriber is replaced), index.event-refresh.test.ts (refreshes exact managed queries by agent), and dashboards-page.test.tsx / dashboards/route.test.ts. Remove the duplicate per-tab replay and wire inventories. Project/place discovery is covered by draft-place-browser.test.ts (discovers places from authenticated hello; reuses current machine info; ignores a late name from the replaced client; settles name discovery) and where-chip.test.ts (keeps matching Local, Devices and Cloud in order). The legacy projects-only E2E checks an already-hidden register button and guidance text, not a separate registration path. Guest settings and offline Stop are covered by chat-pane-session-controls.permissions.node.test.ts (uses exact field permissions), chat-view.test.ts (orders model-dependent patches), chat-pane-stop.test.ts (replays only the captured run once), and chat-run-lifecycle.e2e.test.ts. Keep guest creation, rejected-turn retry, parked panes and live scope revocation. Archive cold adoption remains in chat-pane-state.test.ts (adopts an archived routed row published after the active list omitted it), archive glyph in app-sidebar.stress.browser.test.ts, and unarchive recovery in session-management.archive.e2e.test.ts. Cancel/focus and copy behavior remain in chat-message-confirmation.test.ts and chat-context-copy.test.ts. Keep the live archive event invalidating open controls on both viewports. Merge latest-overlay assertions into the retained wheel scenario. Composer growth/reader policy is owned by chat-transcript-scroll-ownership.test.tsx (preserves intent ownership through composer growth and shrink); nested Details scrolling by chat-footer.e2e.test.ts. Remove only their duplicate E2E layers and unused progress fixtures. Provider wizard success, failure and cancellation remain in model-setup.e2e.test.ts, model-setup-page.test.ts, wizard-runner.test.ts and model-setup-cancel.e2e.test.ts; reopen ownership remains in model-setup-reconnect.e2e.test.ts. Keep native-discovery consent and remote diagnostic copy. Source rows retain one generic key plus the distinct Pi key, defaults, persistence and permission branches; metadata bindings remain in session-source-presentation.node.test.ts. Remove diagnostic screenshots and repeated fixture/assertion data only. * test(scripts): deslop q0110 tests Consolidate workspace omission assertions into the multi-package release report. Replace OCM helper replays with relative-path and private-manifest assertions in the retained packed-root/transitive-workspace install test. The build-identity owner test retains malformed-commit diagnostics. CI-only campaign: oxlint, oxfmt and diff-check pass; tests and coverage intentionally not run. Retain distinct release, integrity, cache and cancellation boundaries; this shard falls short of the requested 40% line reduction. * test(scripts): prune repeated q0110 guards Remove copied mobile schedule, option and display-name inventories and Apple restore-step call-shape checks. Keep generic cache action pinning, write authority, release qualification, environment selection and actual upload execution. Prune same-branch lock metadata and mobile routing variants. The retained packed-root/transitive workspace test also proves non-workspace dependency preservation; remove its helper-level replay. CI-only: oxfmt, plain oxlint and diff-check pass; no tests or coverage run. Cumulative reduction remains below 40%; retain unmatched integrity, admission, recovery and lifecycle coverage. * test(scripts): reuse q0110 qualification fixtures Replace hand-assembled candidate requests and source-admission records with the existing candidatePublicationFixture used by the publish-consumer boundary tests. Retain rehashed missing-child, changed-required-child and original-producer reuse assertions. CI-only: formatting, plain oxlint and diff-check pass. No tests or coverage run. Further cross-file audit found no interchangeable proof for the remaining collector admission, manual update, frozen parity, or checkout cancellation boundaries; the requested 40% reduction remains unmet. * test(ui): deslop q0108 tests Remove copied cloud-worker settings scenarios from the snapshot suite; cloud-workers-settings.e2e.test.ts retains add/edit, reconnect, stale-editor, deletion authority and admin-scope proof. Keep all snapshot flows. Remove the startup-roster replay: sessions-page.test.ts loads the current query before config settles on startup and same-client reconnect, then verifies sidebar hydration cannot replace page state. Keep real-Gateway partial refresh on persisted chat; model-catalog-partial-refresh.e2e.test.ts retains the new-session control path. Factor browser setup and geometry checks, remove write-only proof artifacts, and retain distinct keyboard, rendering and recovery interleavings. Duplicate mutation payload checks remain in session-management.rename.e2e.test.ts and session-management.archive-batch.e2e.test.ts; cloud attachment recovery remains in new-session-page.cloud-startup.recovery.e2e.test.ts. CI-only validation: plain oxlint, oxfmt and diff-check pass. No tests, coverage or builds run. The 40% line-reduction target is not met without removing distinct behaviors; report the honest reduction to the coordinator. * test(scripts): deslop q0109 tests Remove duplicate restart fixture scenarios already owned by test/scripts/upgrade-survivor-restart-fixture.test.ts, including sealed registry bytes, cohort selection, and runtime identity rejection. Factor repeated preparation and download setup. Retain release authority and supply-chain checks, replacing incidental diagnostic-step adjacency with publication ordering. Keep Node major/minor/patch boundary coverage while dropping repeated below-floor versions. CI-only campaign: oxfmt, plain oxlint, and git diff --check passed. Tests and coverage deferred to hosted CI. Shard LOC 3165 -> 2875 (9.16%); declarations 53 -> 51. Remaining safety and lifecycle contracts prevent an honest 40% reduction. * test(scripts): remove redundant q0109 orchestration Replace registry shell orchestration with direct collision and archive-integrity owner tests. Keep shell cohort routing in upgrade-survivor-plugin-registry.test.ts (checks the published plugin cohort) and baseline byte installation in prepublish-plugin-registry-shell.test.ts (keeps published bytes through survivor baseline). Pass the observed PR directly into closeout gates; observation transport is covered by pr-correction-preparation.test.ts and carried observation checks in pr-prepare-gates.test.ts. Reduce release workflow inventories to approval, attestation and publication-order safety invariants. Drop same-branch donor/version and DTS variants. CI-only: plain oxlint, formatting and diff checks passed; no tests or coverage run. Additional LOC 2875 -> 2610; cumulative 3165 -> 2610 (17.54%). Remaining unique safety/lifecycle paths prevent the 40% target without dropping behavior. * test(scripts): consolidate q0109 owner fixtures Run closeout classification through its real policy owner; check-changelog-attributions.test.ts retains prepare_gates rejection wiring. Delete the frozen-record false positive that rejected an invalid branch before reaching record validation. Remove donor and ancestor replays covered by watch-pr-ci-dependencies.test.ts and check-line-cap-ratchet.test.ts. Remove bootstrap transport replay covered by pr-crabbox-gate-transport.test.ts while retaining direct child-status/home cleanup proof. Factor service and shell fixtures, preserving the private-QA flag assertion and every distinct authority/lifecycle path. CI-only: formatting, plain oxlint and diff checks passed; no tests/builds/coverage run. Additional LOC 2610 -> 2440; cumulative 3165 -> 2440 (22.91%). The 40% target remains unmet; no further deletion was justified by retained owner proof. * test(scripts): deslop q0113 tests Consolidate package-copy assertions into publishable-file cases and retain release reference/credit checks through the production ledger round trip. Reuse existing preparation snapshot helpers. * test(scripts): remove redundant q0113 harness and inventories Use the shared managed command fixture for Telegram suite execution; process cleanup is owned and covered by managed-child-process.test.ts and managed-child-process.output.test.ts. Replace the copied suppression inventory with a generic no-blanket-suppression guard. Keep source isolation, packaging, migration, and publication-authority boundaries. * test(pr): remove unused replay signing fixture * test: preserve main contracts after reduction review
296 lines
10 KiB
TypeScript
296 lines
10 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
attributeSpawns,
|
|
classifyNodeSpawns as classify,
|
|
renderMarkdownReport as markdown,
|
|
} from "../../scripts/e2e/lib/bun-only-runtime/node-spawn-ledger.mjs";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const makeStep = (name = "terminals", index = 0, startMs = 100) => ({
|
|
name,
|
|
startMs,
|
|
endMs: startMs + 100,
|
|
sentinelStart: index,
|
|
sentinelEnd: index + 1,
|
|
status: "passed",
|
|
});
|
|
const step = makeStep();
|
|
const trace = {
|
|
childPid: 20,
|
|
pid: 10,
|
|
ppid: 1,
|
|
ts: 150,
|
|
cmd: ["/sentinels/node", "--eval", 'const requiredFlag="";'],
|
|
cwd: "/smoke",
|
|
stack: ["at resolveNodeRuntimeExecutable", "at spawnNodeTerminalPty"],
|
|
};
|
|
const sentinel = {
|
|
name: "node",
|
|
pid: 20,
|
|
ppid: 10,
|
|
cwd: "/smoke",
|
|
argv: trace.cmd.slice(1),
|
|
ancestors: [{ pid: 10, cmdline: "bun\0openclaw.mjs\0gateway\0run\0\0" }],
|
|
};
|
|
const blocker = {
|
|
id: "terminal-pty",
|
|
feature: "Terminals",
|
|
origin: "known-blocker",
|
|
owner: "terminals task",
|
|
callSite: "spawnNodeTerminalPty -> resolveNodeRuntimeExecutable",
|
|
step: "terminals",
|
|
evidence: "spawn",
|
|
match: {
|
|
stack: ["spawnNodeTerminalPty", "resolveNodeRuntimeExecutable"],
|
|
argv: ["--eval", "requiredFlag"],
|
|
},
|
|
};
|
|
type AttributionInput = Parameters<typeof attributeSpawns>[0];
|
|
const attribute = (
|
|
traceRecords: AttributionInput["traceRecords"] = [trace],
|
|
sentinelRecords: AttributionInput["sentinelRecords"] = [sentinel],
|
|
) => attributeSpawns({ sentinelRecords, traceRecords, steps: [step] });
|
|
type Classification = Parameters<typeof classify>[0];
|
|
const classifyWith = (overrides: Partial<Classification> = {}) =>
|
|
classify({ attempts: attribute(), steps: [step], blockers: [blocker], ...overrides });
|
|
|
|
describe("Bun-only Node spawn ledger", () => {
|
|
it.each(["unexpected", "matched", "stack mismatch", "argv mismatch", "failed step"])(
|
|
"classifies %s spawns",
|
|
(kind) => {
|
|
const match =
|
|
kind === "stack mismatch"
|
|
? { stack: ["differentOwner"] }
|
|
: kind === "argv mismatch"
|
|
? { argv: ["--permission"] }
|
|
: blocker.match;
|
|
const result = classifyWith({
|
|
steps: [kind === "failed step" ? { ...step, status: "failed", exitCode: 1 } : step],
|
|
blockers: kind === "unexpected" ? [] : [{ ...blocker, match }],
|
|
});
|
|
expect(result.ok).toBe(kind === "matched");
|
|
if (kind === "matched" || kind === "failed step") {
|
|
expect(result.blockers[0]).toMatchObject({ status: "reproduced", reproducedBy: ["spawn"] });
|
|
expect(markdown(result)).toContain(
|
|
"| terminal-pty | known-blocker | reproduced | 1 | spawn |",
|
|
);
|
|
} else {
|
|
expect(result.unexpectedAttempts).toHaveLength(1);
|
|
}
|
|
if (kind === "failed step") {
|
|
expect(result.failedSteps).toHaveLength(1);
|
|
}
|
|
if (kind === "unexpected") {
|
|
const report = markdown(result);
|
|
expect(report).toContain(JSON.stringify(["node", ...sentinel.argv]));
|
|
expect(report).toContain('ancestor 10: "bun openclaw.mjs gateway run"');
|
|
expect(report).toContain("spawnNodeTerminalPty");
|
|
expect(markdown({ ...result, bunVersion: "1", bunRevision: "rev" })).toContain(
|
|
"Bun 1 (rev)",
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("matches node-host spawns only in the same or later steps", () => {
|
|
const steps = ["gateway", "node-host", "agent"].map((name, i) =>
|
|
makeStep(name, i, 100 + i * 200),
|
|
);
|
|
const argv = ["view", "openclaw@latest"];
|
|
const npm = {
|
|
...trace,
|
|
cmd: ["npm", ...argv],
|
|
stack: ["at fetchNpmPackageTargetStatus"],
|
|
};
|
|
const probe = { ...sentinel, name: "npm", argv: npm.cmd.slice(1) };
|
|
const attempts = attributeSpawns({
|
|
steps,
|
|
traceRecords: [250, 450, 650].map((ts, i) =>
|
|
Object.assign({}, npm, { childPid: 20 + i, ts }),
|
|
),
|
|
sentinelRecords: [0, 1, 2].map((index) =>
|
|
Object.assign({}, probe, { pid: 20 + index, index }),
|
|
),
|
|
});
|
|
expect(attempts.map((attempt) => attempt.step)).toEqual(["gateway", "node-host", "agent"]);
|
|
const result = classifyWith({
|
|
attempts,
|
|
steps,
|
|
blockers: [
|
|
{
|
|
...blocker,
|
|
step: "node-host",
|
|
match: { stack: ["fetchNpmPackageTargetStatus"], argv },
|
|
},
|
|
],
|
|
});
|
|
expect(result).toMatchObject({
|
|
blockers: [{ matchedAttempts: attempts.slice(1) }],
|
|
staleBlockers: [],
|
|
});
|
|
expect(result.unexpectedAttempts).toEqual([attempts[0]]);
|
|
});
|
|
|
|
it.each([{ evidence: "failure" }, { evidence: ["spawn", "failure"] }])(
|
|
"matches cached failure evidence %j",
|
|
({ evidence }) => {
|
|
const result = classifyWith({
|
|
attempts: [],
|
|
steps: [{ ...step, stderr: "Node required on Bun" }],
|
|
blockers: [{ ...blocker, evidence, failure: "Node required" }],
|
|
});
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
blockers: [{ status: "reproduced", reproducedBy: ["failure"] }],
|
|
});
|
|
if (evidence === "failure") {
|
|
const withSpawn = classify({ ...result, attempts: attribute() });
|
|
expect(withSpawn.unexpectedAttempts).toHaveLength(1);
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each(["stale", "not-exercised"])("reports a %s blocker without spawn evidence", (status) => {
|
|
const exercised = status === "stale";
|
|
const reason = "Desktop provider is not enabled.";
|
|
const result = classifyWith({
|
|
attempts: [],
|
|
steps: exercised ? [step] : [],
|
|
blockers: [exercised ? blocker : { ...blocker, step: null, notExercisedReason: reason }],
|
|
});
|
|
expect(result).toMatchObject({ ok: !exercised, blockers: [{ status }] });
|
|
expect(result.staleBlockers).toHaveLength(exercised ? 1 : 0);
|
|
expect(markdown(result)).toContain(
|
|
exercised ? "Delete this entry from expected-node-blockers.json." : reason,
|
|
);
|
|
});
|
|
|
|
it("matches path-qualified launchers at the exact sentinel path", () => {
|
|
const shellTrace = { ...trace, childPid: 15, cmd: ["sh", "-c", "/usr/local/bin/node -v"] };
|
|
const ancestors = [{ pid: 15, cmdline: "sh" }];
|
|
const masked = { ...sentinel, exe: "/usr/local/bin/node", argv: ["-v"], ancestors };
|
|
expect(attribute([shellTrace], [masked])).toHaveLength(1);
|
|
const elsewhere = { ...masked, exe: "/sentinels/node" };
|
|
expect(attribute([shellTrace], [elsewhere])).toHaveLength(2);
|
|
});
|
|
|
|
it.each([
|
|
["unrelated", "node --version"],
|
|
["descendants", "node -v; npm -v"],
|
|
["exec", "exec node --version"],
|
|
])("attributes %s shell stacks", (kind, command) => {
|
|
const shellTrace = {
|
|
...trace,
|
|
childPid: kind === "exec" ? trace.childPid : 15,
|
|
cmd: ["/bin/sh", "-c", command],
|
|
};
|
|
const ancestors = [
|
|
{ pid: 15, cmdline: "sh" },
|
|
{ pid: 10, cmdline: "bun" },
|
|
];
|
|
const sentinels =
|
|
kind === "descendants"
|
|
? [
|
|
{ ...sentinel, ppid: 15, argv: ["-v"], ancestors },
|
|
{ ...sentinel, pid: 21, ppid: 15, name: "npm", argv: ["-v"], ancestors },
|
|
]
|
|
: [kind === "exec" ? { ...sentinel, argv: ["--version"] } : sentinel];
|
|
const attempts = attribute([shellTrace], sentinels);
|
|
if (kind === "unrelated") {
|
|
expect(attempts).toMatchObject([{ sentinel, stack: [] }]);
|
|
expect(attempts[0]).not.toHaveProperty("trace");
|
|
} else {
|
|
expect(attempts).toMatchObject(
|
|
kind === "descendants"
|
|
? [
|
|
{ trace: shellTrace, sentinel: { name: "node" } },
|
|
{ trace: shellTrace, sentinel: { name: "npm" } },
|
|
]
|
|
: [{ trace: shellTrace, stack: trace.stack, sentinel: { pid: trace.childPid } }],
|
|
);
|
|
}
|
|
});
|
|
|
|
it.each([null, 99])("keeps a trace with child PID %s separate from the sentinel", (childPid) => {
|
|
const attempts = attribute([{ ...trace, childPid, errorCode: "ENOENT" }]);
|
|
expect(attempts).toMatchObject([
|
|
{ sentinel, stack: [] },
|
|
{ trace: { childPid, errorCode: "ENOENT" } },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("Bun spawn trace preload", () => {
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
|
|
async function preload(spawn: (...args: unknown[]) => unknown) {
|
|
const tracePath = path.join(tempDirs.make("bun-spawn-trace-"), "trace.jsonl");
|
|
let runtime = (
|
|
globalThis as typeof globalThis & {
|
|
Bun?: { spawn: typeof spawn; spawnSync: typeof spawn };
|
|
}
|
|
).Bun;
|
|
if (runtime) {
|
|
for (const method of ["spawn", "spawnSync"] as const) {
|
|
vi.spyOn(runtime, method).mockImplementation(spawn);
|
|
}
|
|
} else {
|
|
runtime = { spawn, spawnSync: spawn };
|
|
vi.stubGlobal("Bun", runtime);
|
|
}
|
|
vi.stubEnv("OPENCLAW_BUN_ONLY_SPAWN_TRACE", tracePath);
|
|
vi.resetModules();
|
|
await import("../../scripts/e2e/lib/bun-only-runtime/spawn-trace-preload.mjs");
|
|
return { tracePath, runtime };
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.unstubAllEnvs();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it.each([
|
|
["spawn", false],
|
|
["spawnSync", false],
|
|
["spawn", true],
|
|
["spawnSync", true],
|
|
] as const)("records %s with failure=%s without changing its outcome", async (method, failed) => {
|
|
const clock = vi.spyOn(Date, "now").mockReturnValue(100);
|
|
const child = { pid: 42 };
|
|
const error = Object.assign(new Error("not found"), { code: "ENOENT" });
|
|
const original = vi.fn(() => {
|
|
if (failed) {
|
|
throw error;
|
|
}
|
|
expect(existsSync(tracePath)).toBe(false);
|
|
clock.mockReturnValue(200);
|
|
return child;
|
|
});
|
|
const { tracePath, runtime } = await preload(original);
|
|
const cmd = failed
|
|
? ["node", "--version"]
|
|
: ["/bin/sh", "-c", "/usr/bin/node --version; /opt/tools/npm --version"];
|
|
const options = { cwd: "/smoke" };
|
|
if (failed) {
|
|
expect(() => Reflect.apply(runtime[method], runtime, [{ cmd }])).toThrow(error);
|
|
} else {
|
|
expect(Reflect.apply(runtime[method], runtime, [cmd, options])).toBe(child);
|
|
expect(original).toHaveBeenCalledExactlyOnceWith(cmd, options);
|
|
expect(original.mock.contexts).toEqual([runtime]);
|
|
}
|
|
expect(JSON.parse(readFileSync(tracePath, "utf8"))).toMatchObject(
|
|
failed
|
|
? { childPid: null, errorCode: "ENOENT", cmd }
|
|
: { childPid: 42, ts: 100, cmd, cwd: "/smoke", stack: expect.any(Array) },
|
|
);
|
|
});
|
|
|
|
it("does not trace unrelated shell commands", async () => {
|
|
const { tracePath, runtime } = await preload(vi.fn(() => ({ pid: 7 })));
|
|
runtime.spawn(["/bin/sh", "-c", "echo nodejs-like && nodemon --help"]);
|
|
expect(existsSync(tracePath)).toBe(false);
|
|
});
|
|
});
|