mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
test(qa): expand docker lane adapter (#97937)
* test(qa): expand docker lane adapter * Extract QA Docker lane fixture
This commit is contained in:
parent
18b2ff683f
commit
fd3f354f46
19 changed files with 293 additions and 411 deletions
|
|
@ -15,19 +15,6 @@ export {
|
|||
type LiveTransportScenarioDefinition,
|
||||
type LiveTransportStandardScenarioId,
|
||||
} from "openclaw/plugin-sdk/qa-live-transport-scenarios";
|
||||
export {
|
||||
assertQaTransportSupportsScenario,
|
||||
defineQaTransportScenario,
|
||||
filterQaTransportScenariosForTransport,
|
||||
findUnsupportedQaTransportScenarioRequirements,
|
||||
mergeQaTransportScenarioRequirements,
|
||||
qaTransportRequirementsForStandardScenario,
|
||||
type QaTransportCapabilityName,
|
||||
type QaTransportScenarioDefinition,
|
||||
type QaTransportScenarioDefinitionInput,
|
||||
type QaTransportScenarioRequirements,
|
||||
type QaTransportScenarioUnsupportedRequirements,
|
||||
} from "../../qa-transport-scenarios.js";
|
||||
|
||||
export type LiveTransportCoverageMember = {
|
||||
scenarioId?: string;
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
// Qa Lab tests cover transport-backed scenario metadata.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createQaBusState } from "./bus-state.js";
|
||||
import { createQaChannelTransport } from "./qa-channel-transport.js";
|
||||
import {
|
||||
assertQaTransportSupportsScenario,
|
||||
defineQaTransportScenario,
|
||||
filterQaTransportScenariosForTransport,
|
||||
findUnsupportedQaTransportScenarioRequirements,
|
||||
mergeQaTransportScenarioRequirements,
|
||||
qaTransportRequirementsForStandardScenario,
|
||||
} from "./qa-transport-scenarios.js";
|
||||
import type { QaTransportAdapter } from "./qa-transport.js";
|
||||
|
||||
type ScenarioTransportFixture = Pick<
|
||||
QaTransportAdapter,
|
||||
"capabilities" | "id" | "supportedActions"
|
||||
>;
|
||||
|
||||
function createScenarioTransportFixture(
|
||||
supportedActions: QaTransportAdapter["supportedActions"],
|
||||
): ScenarioTransportFixture {
|
||||
const transport = createQaChannelTransport(createQaBusState());
|
||||
return {
|
||||
capabilities: transport.capabilities,
|
||||
id: "crabline",
|
||||
supportedActions,
|
||||
};
|
||||
}
|
||||
|
||||
describe("qa transport scenarios", () => {
|
||||
it("maps standard live transport buckets onto existing transport capabilities", () => {
|
||||
expect(qaTransportRequirementsForStandardScenario("canary")).toEqual({
|
||||
capabilities: ["assertNoFailureReplies", "sendInboundMessage", "waitForOutboundMessage"],
|
||||
});
|
||||
expect(qaTransportRequirementsForStandardScenario("restart-resume")).toEqual({
|
||||
capabilities: [
|
||||
"assertNoFailureReplies",
|
||||
"sendInboundMessage",
|
||||
"waitForOutboundMessage",
|
||||
"waitForReady",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("merges standard and explicit requirements without duplicating names", () => {
|
||||
const scenario = defineQaTransportScenario({
|
||||
id: "slack-thread-follow-up",
|
||||
standardId: "thread-follow-up",
|
||||
timeoutMs: 30_000,
|
||||
title: "Thread follow-up",
|
||||
transportRequirements: {
|
||||
actions: ["thread-create", "thread-create"],
|
||||
capabilities: ["waitForOutboundMessage", "waitForCondition"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(scenario.transportRequirements).toEqual({
|
||||
actions: ["thread-create"],
|
||||
capabilities: [
|
||||
"assertNoFailureReplies",
|
||||
"sendInboundMessage",
|
||||
"waitForOutboundMessage",
|
||||
"waitForCondition",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("can merge requirements independently for shared scenario builders", () => {
|
||||
expect(
|
||||
mergeQaTransportScenarioRequirements([
|
||||
{ actions: ["react"], capabilities: ["sendInboundMessage"] },
|
||||
{ actions: ["react", "delete"], capabilities: ["sendInboundMessage", "waitForReady"] },
|
||||
]),
|
||||
).toEqual({
|
||||
actions: ["react", "delete"],
|
||||
capabilities: ["sendInboundMessage", "waitForReady"],
|
||||
});
|
||||
});
|
||||
|
||||
it("validates a scenario against a real qa-channel transport", () => {
|
||||
const transport = createQaChannelTransport(createQaBusState());
|
||||
const scenario = defineQaTransportScenario({
|
||||
id: "qa-channel-reaction",
|
||||
timeoutMs: 30_000,
|
||||
title: "Reaction action",
|
||||
transportRequirements: {
|
||||
actions: ["react"],
|
||||
capabilities: ["executeGenericAction", "waitForCondition"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => assertQaTransportSupportsScenario({ scenario, transport })).not.toThrow();
|
||||
});
|
||||
|
||||
it("reports unsupported transport actions without executing the action", () => {
|
||||
const transport = createScenarioTransportFixture([]);
|
||||
const scenario = defineQaTransportScenario({
|
||||
id: "reaction-action",
|
||||
timeoutMs: 30_000,
|
||||
title: "Reaction action",
|
||||
transportRequirements: {
|
||||
actions: ["react"],
|
||||
capabilities: ["executeGenericAction"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(findUnsupportedQaTransportScenarioRequirements({ scenario, transport })).toEqual({
|
||||
actions: ["react"],
|
||||
capabilities: [],
|
||||
});
|
||||
expect(() => assertQaTransportSupportsScenario({ scenario, transport })).toThrow(
|
||||
"QA transport crabline cannot run scenario reaction-action; unsupported actions: react",
|
||||
);
|
||||
});
|
||||
|
||||
it("filters scenarios by the supplied transport contract", () => {
|
||||
const transport = createScenarioTransportFixture([]);
|
||||
const canary = defineQaTransportScenario({
|
||||
id: "canary",
|
||||
standardId: "canary",
|
||||
timeoutMs: 30_000,
|
||||
title: "Canary",
|
||||
});
|
||||
const reaction = defineQaTransportScenario({
|
||||
id: "reaction",
|
||||
timeoutMs: 30_000,
|
||||
title: "Reaction",
|
||||
transportRequirements: { actions: ["react"] },
|
||||
});
|
||||
|
||||
expect(
|
||||
filterQaTransportScenariosForTransport({
|
||||
scenarios: [canary, reaction],
|
||||
transport,
|
||||
}),
|
||||
).toEqual([canary]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
// Qa Lab plugin module defines scenario metadata for transport-backed QA runs.
|
||||
import type {
|
||||
LiveTransportScenarioDefinition,
|
||||
LiveTransportStandardScenarioId,
|
||||
} from "openclaw/plugin-sdk/qa-live-transport-scenarios";
|
||||
import type {
|
||||
QaTransportActionName,
|
||||
QaTransportAdapter,
|
||||
QaTransportCapabilities,
|
||||
} from "./qa-transport.js";
|
||||
|
||||
export type QaTransportCapabilityName = keyof QaTransportCapabilities;
|
||||
|
||||
export type QaTransportScenarioRequirements = {
|
||||
actions?: readonly QaTransportActionName[];
|
||||
capabilities?: readonly QaTransportCapabilityName[];
|
||||
};
|
||||
|
||||
export type QaTransportScenarioDefinition<TId extends string = string> =
|
||||
LiveTransportScenarioDefinition<TId> & {
|
||||
transportRequirements: QaTransportScenarioRequirements;
|
||||
};
|
||||
|
||||
export type QaTransportScenarioDefinitionInput<TId extends string = string> =
|
||||
LiveTransportScenarioDefinition<TId> & {
|
||||
transportRequirements?: QaTransportScenarioRequirements;
|
||||
};
|
||||
|
||||
export type QaTransportScenarioUnsupportedRequirements = {
|
||||
actions: QaTransportActionName[];
|
||||
capabilities: QaTransportCapabilityName[];
|
||||
};
|
||||
|
||||
const TEXT_REPLY_CAPABILITIES = [
|
||||
"assertNoFailureReplies",
|
||||
"sendInboundMessage",
|
||||
"waitForOutboundMessage",
|
||||
] as const satisfies readonly QaTransportCapabilityName[];
|
||||
|
||||
const NO_REPLY_CAPABILITIES = [
|
||||
"assertNoFailureReplies",
|
||||
"sendInboundMessage",
|
||||
"waitForCondition",
|
||||
] as const satisfies readonly QaTransportCapabilityName[];
|
||||
|
||||
const STANDARD_SCENARIO_REQUIREMENTS = {
|
||||
canary: { capabilities: TEXT_REPLY_CAPABILITIES },
|
||||
"mention-gating": { capabilities: NO_REPLY_CAPABILITIES },
|
||||
"allowlist-block": { capabilities: NO_REPLY_CAPABILITIES },
|
||||
"top-level-reply-shape": { capabilities: TEXT_REPLY_CAPABILITIES },
|
||||
"quote-reply": { capabilities: TEXT_REPLY_CAPABILITIES },
|
||||
"restart-resume": {
|
||||
capabilities: [...TEXT_REPLY_CAPABILITIES, "waitForReady"],
|
||||
},
|
||||
"thread-follow-up": { capabilities: TEXT_REPLY_CAPABILITIES },
|
||||
"thread-isolation": {
|
||||
capabilities: [...TEXT_REPLY_CAPABILITIES, "waitForCondition"],
|
||||
},
|
||||
"reaction-observation": {
|
||||
capabilities: ["getNormalizedMessageState", "waitForCondition"],
|
||||
},
|
||||
"help-command": { capabilities: TEXT_REPLY_CAPABILITIES },
|
||||
} as const satisfies Record<LiveTransportStandardScenarioId, QaTransportScenarioRequirements>;
|
||||
|
||||
function uniqueInOrder<T extends string>(values: readonly T[]) {
|
||||
const seen = new Set<T>();
|
||||
const unique: T[] = [];
|
||||
for (const value of values) {
|
||||
if (!seen.has(value)) {
|
||||
seen.add(value);
|
||||
unique.push(value);
|
||||
}
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
|
||||
export function mergeQaTransportScenarioRequirements(
|
||||
requirements: readonly QaTransportScenarioRequirements[],
|
||||
): QaTransportScenarioRequirements {
|
||||
return {
|
||||
actions: uniqueInOrder(requirements.flatMap((requirement) => requirement.actions ?? [])),
|
||||
capabilities: uniqueInOrder(
|
||||
requirements.flatMap((requirement) => requirement.capabilities ?? []),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function qaTransportRequirementsForStandardScenario(
|
||||
standardId: LiveTransportStandardScenarioId,
|
||||
): QaTransportScenarioRequirements {
|
||||
return STANDARD_SCENARIO_REQUIREMENTS[standardId];
|
||||
}
|
||||
|
||||
export function defineQaTransportScenario<TId extends string>(
|
||||
input: QaTransportScenarioDefinitionInput<TId>,
|
||||
): QaTransportScenarioDefinition<TId> {
|
||||
const standardRequirements = input.standardId
|
||||
? qaTransportRequirementsForStandardScenario(input.standardId)
|
||||
: {};
|
||||
return {
|
||||
...input,
|
||||
transportRequirements: mergeQaTransportScenarioRequirements([
|
||||
standardRequirements,
|
||||
input.transportRequirements ?? {},
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
export function findUnsupportedQaTransportScenarioRequirements(params: {
|
||||
scenario: QaTransportScenarioDefinition;
|
||||
transport: Pick<QaTransportAdapter, "capabilities" | "supportedActions">;
|
||||
}): QaTransportScenarioUnsupportedRequirements {
|
||||
const supportedActions = new Set(params.transport.supportedActions);
|
||||
return {
|
||||
actions: (params.scenario.transportRequirements.actions ?? []).filter(
|
||||
(action) => !supportedActions.has(action),
|
||||
),
|
||||
capabilities: (params.scenario.transportRequirements.capabilities ?? []).filter(
|
||||
(capability) => typeof params.transport.capabilities[capability] !== "function",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function assertQaTransportSupportsScenario(params: {
|
||||
scenario: QaTransportScenarioDefinition;
|
||||
transport: Pick<QaTransportAdapter, "capabilities" | "id" | "supportedActions">;
|
||||
}) {
|
||||
const unsupported = findUnsupportedQaTransportScenarioRequirements(params);
|
||||
const problems = [
|
||||
unsupported.capabilities.length > 0
|
||||
? `missing capabilities: ${unsupported.capabilities.join(", ")}`
|
||||
: undefined,
|
||||
unsupported.actions.length > 0
|
||||
? `unsupported actions: ${unsupported.actions.join(", ")}`
|
||||
: undefined,
|
||||
].filter((problem): problem is string => Boolean(problem));
|
||||
if (problems.length > 0) {
|
||||
throw new Error(
|
||||
`QA transport ${params.transport.id} cannot run scenario ${params.scenario.id}; ${problems.join("; ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function filterQaTransportScenariosForTransport<
|
||||
TScenario extends QaTransportScenarioDefinition,
|
||||
>(params: {
|
||||
scenarios: readonly TScenario[];
|
||||
transport: Pick<QaTransportAdapter, "capabilities" | "supportedActions">;
|
||||
}): TScenario[] {
|
||||
return params.scenarios.filter((scenario) => {
|
||||
const unsupported = findUnsupportedQaTransportScenarioRequirements({
|
||||
scenario,
|
||||
transport: params.transport,
|
||||
});
|
||||
return unsupported.actions.length === 0 && unsupported.capabilities.length === 0;
|
||||
});
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ scenario:
|
|||
- scripts/e2e/lib/release-scenarios/write-marketplace.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: scripts/qa/docker-e2e-lane.ts
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the release-plugin-marketplace Docker E2E lane as QA Lab evidence for ClawHub marketplace list coverage.
|
||||
args:
|
||||
- --lane
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ scenario:
|
|||
- test/e2e/qa-lab/runtime/agent-bundle-mcp-tools-docker-client.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: scripts/qa/docker-e2e-lane.ts
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing agent-bundle-mcp-tools Docker E2E lane for Docker-backed agent tooling coverage.
|
||||
args:
|
||||
- --lane
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ scenario:
|
|||
- scripts/e2e/lib/fixture.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: scripts/qa/docker-e2e-lane.ts
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing agents-delete-shared-workspace Docker E2E lane for Docker-hosted agent workspace safety coverage.
|
||||
args:
|
||||
- --lane
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ scenario:
|
|||
- scripts/e2e/crestodian-first-run-spec.json
|
||||
execution:
|
||||
kind: script
|
||||
path: scripts/qa/docker-e2e-lane.ts
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing Crestodian first-run Docker E2E lane and records primary Docker first-run onboarding evidence; the Raspberry Pi link is secondary because this is generic Linux/container first-run proof, not Pi hardware proof.
|
||||
args:
|
||||
- --lane
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ scenario:
|
|||
- scripts/e2e/lib/gateway-network/client.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: scripts/qa/docker-e2e-lane.ts
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing gateway-network Docker E2E lane for compose-style container network access coverage.
|
||||
args:
|
||||
- --lane
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ scenario:
|
|||
- scripts/e2e/lib/npm-onboard-channel-agent/assertions.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: scripts/qa/docker-e2e-lane.ts
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing npm-onboard-channel-agent Docker E2E lane for package-installed Docker agent setup and turn coverage.
|
||||
args:
|
||||
- --lane
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ scenario:
|
|||
- scripts/e2e/lib/release-user-journey/assertions.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: scripts/qa/docker-e2e-lane.ts
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing release-upgrade-user-journey Docker E2E lane and records QA Lab evidence for release-path upgrade coverage.
|
||||
args:
|
||||
- --lane
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ scenario:
|
|||
- scripts/e2e/lib/release-user-journey/assertions.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: scripts/qa/docker-e2e-lane.ts
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing release-user-journey Docker E2E lane and records QA Lab evidence for release-path install coverage.
|
||||
args:
|
||||
- --lane
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ scenario:
|
|||
- scripts/e2e/lib/update-channel-switch/assertions.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: scripts/qa/docker-e2e-lane.ts
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing update-channel-switch Docker E2E lane and records QA Lab evidence for install-kind switching and update status coverage.
|
||||
args:
|
||||
- --lane
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ scenario:
|
|||
- src/cli/update-cli/post-core-plugin-convergence.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: scripts/qa/docker-e2e-lane.ts
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing update-migration Docker E2E lane and records QA Lab evidence for plugin convergence coverage.
|
||||
args:
|
||||
- --lane
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ scenario:
|
|||
- scripts/e2e/lib/upgrade-survivor/update-restart-auth.sh
|
||||
execution:
|
||||
kind: script
|
||||
path: scripts/qa/docker-e2e-lane.ts
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing update-restart-auth Docker E2E lane and records QA Lab evidence for managed Gateway restart coverage.
|
||||
args:
|
||||
- --lane
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ scenario:
|
|||
- scripts/e2e/lib/upgrade-survivor/assertions.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: scripts/qa/docker-e2e-lane.ts
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing upgrade-survivor Docker E2E lane and records QA Lab evidence for update status/RPC survival coverage.
|
||||
args:
|
||||
- --lane
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
// Runs an existing Docker E2E lane through the QA Lab script scenario contract.
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
type Lane = {
|
||||
env?: Record<string, string>;
|
||||
script: string;
|
||||
};
|
||||
|
||||
const lanes: Record<string, Lane> = {
|
||||
"agent-bundle-mcp-tools": {
|
||||
script: "scripts/e2e/agent-bundle-mcp-tools-docker.sh",
|
||||
},
|
||||
"agents-delete-shared-workspace": {
|
||||
script: "scripts/e2e/agents-delete-shared-workspace-docker.sh",
|
||||
},
|
||||
"crestodian-first-run": {
|
||||
script: "scripts/e2e/crestodian-first-run-docker.sh",
|
||||
},
|
||||
"gateway-network": {
|
||||
script: "scripts/e2e/gateway-network-docker.sh",
|
||||
},
|
||||
"npm-onboard-channel-agent": {
|
||||
script: "scripts/e2e/npm-onboard-channel-agent-docker.sh",
|
||||
},
|
||||
"release-upgrade-user-journey": {
|
||||
script: "scripts/e2e/release-upgrade-user-journey-docker.sh",
|
||||
},
|
||||
"release-plugin-marketplace": {
|
||||
script: "scripts/e2e/release-plugin-marketplace-docker.sh",
|
||||
},
|
||||
"release-user-journey": {
|
||||
script: "scripts/e2e/release-user-journey-docker.sh",
|
||||
},
|
||||
"update-channel-switch": {
|
||||
script: "scripts/e2e/update-channel-switch-docker.sh",
|
||||
},
|
||||
"update-migration": {
|
||||
env: {
|
||||
OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE: "1",
|
||||
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:
|
||||
process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC ?? "openclaw@2026.4.23",
|
||||
OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:
|
||||
process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIO ?? "plugin-deps-cleanup",
|
||||
},
|
||||
script: "scripts/e2e/upgrade-survivor-docker.sh",
|
||||
},
|
||||
"update-restart-auth": {
|
||||
env: {
|
||||
OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE: "auto-auth",
|
||||
OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:
|
||||
process.env.OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT ?? "1500s",
|
||||
},
|
||||
script: "scripts/e2e/upgrade-survivor-docker.sh",
|
||||
},
|
||||
"upgrade-survivor": {
|
||||
script: "scripts/e2e/upgrade-survivor-docker.sh",
|
||||
},
|
||||
};
|
||||
|
||||
function laneArg(argv: string[]) {
|
||||
const index = argv.indexOf("--lane");
|
||||
if (index === -1) {
|
||||
throw new Error("--lane is required");
|
||||
}
|
||||
const lane = argv[index + 1];
|
||||
if (!lane || lane.startsWith("-")) {
|
||||
throw new Error("--lane requires a value");
|
||||
}
|
||||
return lane;
|
||||
}
|
||||
|
||||
const laneName = laneArg(process.argv.slice(2));
|
||||
const lane = lanes[laneName];
|
||||
if (!lane) {
|
||||
throw new Error(`unknown Docker E2E lane: ${laneName}`);
|
||||
}
|
||||
|
||||
const result = spawnSync("bash", [lane.script], {
|
||||
env: { ...process.env, ...lane.env },
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
if (result.signal) {
|
||||
process.kill(process.pid, result.signal);
|
||||
}
|
||||
process.exit(result.status ?? 1);
|
||||
84
test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.test.ts
Normal file
84
test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.test.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Docker E2E lane fixture tests keep QA scenario dispatch policy reusable.
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
formatQaDockerE2eLaneUsage,
|
||||
listQaDockerE2eLaneNames,
|
||||
parseQaDockerE2eLaneArgs,
|
||||
resolveQaDockerE2eLane,
|
||||
runQaDockerE2eLane,
|
||||
} from "./docker-e2e-lane.fixture.ts";
|
||||
|
||||
describe("QA Docker E2E lane fixture", () => {
|
||||
it("lists known Docker lanes for scenario wrappers", () => {
|
||||
expect(listQaDockerE2eLaneNames()).toEqual(
|
||||
expect.arrayContaining([
|
||||
"agent-bundle-mcp-tools",
|
||||
"crestodian-first-run",
|
||||
"gateway-network",
|
||||
"release-plugin-marketplace",
|
||||
"update-migration",
|
||||
"update-restart-auth",
|
||||
]),
|
||||
);
|
||||
expect(listQaDockerE2eLaneNames()).toEqual([...listQaDockerE2eLaneNames()].sort());
|
||||
});
|
||||
|
||||
it("parses help, list, and lane arguments", () => {
|
||||
expect(parseQaDockerE2eLaneArgs(["--help"])).toEqual({ kind: "help" });
|
||||
expect(parseQaDockerE2eLaneArgs(["--list"])).toEqual({ kind: "list" });
|
||||
expect(parseQaDockerE2eLaneArgs(["--lane", "gateway-network"])).toEqual({
|
||||
kind: "run",
|
||||
laneName: "gateway-network",
|
||||
});
|
||||
|
||||
expect(() => parseQaDockerE2eLaneArgs([])).toThrow("--lane is required");
|
||||
expect(() => parseQaDockerE2eLaneArgs(["--lane"])).toThrow("--lane requires a value");
|
||||
});
|
||||
|
||||
it("renders usage from the shared lane registry", () => {
|
||||
const usage = formatQaDockerE2eLaneUsage("node qa-docker.js");
|
||||
|
||||
expect(usage).toContain("Usage: node qa-docker.js --lane <name>");
|
||||
expect(usage).toContain(" - gateway-network");
|
||||
expect(usage).toContain(" - update-restart-auth");
|
||||
});
|
||||
|
||||
it("resolves lane-specific environment overlays at run time", () => {
|
||||
const updateMigration = resolveQaDockerE2eLane("update-migration", {
|
||||
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: "openclaw@custom",
|
||||
OPENCLAW_UPGRADE_SURVIVOR_SCENARIO: "custom-scenario",
|
||||
});
|
||||
|
||||
expect(updateMigration.script).toBe("scripts/e2e/upgrade-survivor-docker.sh");
|
||||
expect(updateMigration.env.OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE).toBe("1");
|
||||
expect(updateMigration.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC).toBe("openclaw@custom");
|
||||
expect(updateMigration.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIO).toBe("custom-scenario");
|
||||
|
||||
const updateRestartAuth = resolveQaDockerE2eLane("update-restart-auth", {});
|
||||
expect(updateRestartAuth.env.OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE).toBe("auto-auth");
|
||||
expect(updateRestartAuth.env.OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT).toBe("1500s");
|
||||
});
|
||||
|
||||
it("dispatches through bash without running Docker in fixture tests", () => {
|
||||
const spawn = vi.fn(() => ({ signal: null, status: 0 }));
|
||||
|
||||
expect(runQaDockerE2eLane("gateway-network", { env: { EXTRA: "1" }, spawn })).toEqual({
|
||||
signal: null,
|
||||
status: 0,
|
||||
});
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith("bash", ["scripts/e2e/gateway-network-docker.sh"], {
|
||||
env: { EXTRA: "1" },
|
||||
stdio: "inherit",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects unknown lanes before spawning", () => {
|
||||
const spawn = vi.fn(() => ({ signal: null, status: 0 }));
|
||||
|
||||
expect(() => runQaDockerE2eLane("missing-lane", { spawn })).toThrow(
|
||||
"unknown Docker E2E lane: missing-lane",
|
||||
);
|
||||
expect(spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
169
test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts
Normal file
169
test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
// Shared QA Lab fixture for dispatching existing Docker E2E lanes.
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
export type QaDockerE2eLaneDefinition = {
|
||||
env?: (env: NodeJS.ProcessEnv) => Record<string, string>;
|
||||
script: string;
|
||||
};
|
||||
|
||||
type QaDockerE2eLaneRunResult = {
|
||||
error?: Error;
|
||||
signal: NodeJS.Signals | null;
|
||||
status: number | null;
|
||||
};
|
||||
|
||||
type SpawnQaDockerE2eLane = (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: { env: NodeJS.ProcessEnv; stdio: "inherit" },
|
||||
) => QaDockerE2eLaneRunResult;
|
||||
|
||||
export const QA_DOCKER_E2E_LANES = {
|
||||
"agent-bundle-mcp-tools": {
|
||||
script: "scripts/e2e/agent-bundle-mcp-tools-docker.sh",
|
||||
},
|
||||
"agents-delete-shared-workspace": {
|
||||
script: "scripts/e2e/agents-delete-shared-workspace-docker.sh",
|
||||
},
|
||||
"bundled-plugin-install-uninstall": {
|
||||
script: "scripts/e2e/bundled-plugin-install-uninstall-docker.sh",
|
||||
},
|
||||
"crestodian-first-run": {
|
||||
script: "scripts/e2e/crestodian-first-run-docker.sh",
|
||||
},
|
||||
"docker-build-image": {
|
||||
script: "scripts/e2e/build-image.sh",
|
||||
},
|
||||
"gateway-network": {
|
||||
script: "scripts/e2e/gateway-network-docker.sh",
|
||||
},
|
||||
"npm-onboard-channel-agent": {
|
||||
script: "scripts/e2e/npm-onboard-channel-agent-docker.sh",
|
||||
},
|
||||
"openai-chat-tools": {
|
||||
script: "scripts/e2e/openai-chat-tools-docker.sh",
|
||||
},
|
||||
"openai-web-search-minimal": {
|
||||
script: "scripts/e2e/openai-web-search-minimal-docker.sh",
|
||||
},
|
||||
openwebui: {
|
||||
script: "scripts/e2e/openwebui-docker.sh",
|
||||
},
|
||||
"plugin-lifecycle-matrix": {
|
||||
script: "scripts/e2e/plugin-lifecycle-matrix-docker.sh",
|
||||
},
|
||||
"release-plugin-marketplace": {
|
||||
script: "scripts/e2e/release-plugin-marketplace-docker.sh",
|
||||
},
|
||||
"release-upgrade-user-journey": {
|
||||
script: "scripts/e2e/release-upgrade-user-journey-docker.sh",
|
||||
},
|
||||
"release-user-journey": {
|
||||
script: "scripts/e2e/release-user-journey-docker.sh",
|
||||
},
|
||||
"update-channel-switch": {
|
||||
script: "scripts/e2e/update-channel-switch-docker.sh",
|
||||
},
|
||||
"update-migration": {
|
||||
env: (env) => ({
|
||||
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:
|
||||
env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC ?? "openclaw@2026.4.23",
|
||||
OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE: "1",
|
||||
OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:
|
||||
env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIO ?? "plugin-deps-cleanup",
|
||||
}),
|
||||
script: "scripts/e2e/upgrade-survivor-docker.sh",
|
||||
},
|
||||
"update-restart-auth": {
|
||||
env: (env) => ({
|
||||
OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:
|
||||
env.OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT ?? "1500s",
|
||||
OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE: "auto-auth",
|
||||
}),
|
||||
script: "scripts/e2e/upgrade-survivor-docker.sh",
|
||||
},
|
||||
"upgrade-survivor": {
|
||||
script: "scripts/e2e/upgrade-survivor-docker.sh",
|
||||
},
|
||||
} satisfies Record<string, QaDockerE2eLaneDefinition>;
|
||||
|
||||
export type QaDockerE2eLaneName = keyof typeof QA_DOCKER_E2E_LANES;
|
||||
|
||||
export type QaDockerE2eLaneArgs =
|
||||
| { kind: "help" }
|
||||
| { kind: "list" }
|
||||
| { kind: "run"; laneName: string };
|
||||
|
||||
export type ResolvedQaDockerE2eLane = {
|
||||
env: NodeJS.ProcessEnv;
|
||||
name: QaDockerE2eLaneName;
|
||||
script: string;
|
||||
};
|
||||
|
||||
export function listQaDockerE2eLaneNames(): QaDockerE2eLaneName[] {
|
||||
return Object.keys(QA_DOCKER_E2E_LANES).toSorted() as QaDockerE2eLaneName[];
|
||||
}
|
||||
|
||||
export function formatQaDockerE2eLaneUsage(
|
||||
entrypoint = "node --import tsx test/e2e/qa-lab/runtime/docker-e2e-lane.ts",
|
||||
): string {
|
||||
return [
|
||||
`Usage: ${entrypoint} --lane <name>`,
|
||||
"",
|
||||
"Known lanes:",
|
||||
...listQaDockerE2eLaneNames().map((lane) => ` - ${lane}`),
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function parseQaDockerE2eLaneArgs(argv: string[]): QaDockerE2eLaneArgs {
|
||||
if (argv.includes("--help") || argv.includes("-h")) {
|
||||
return { kind: "help" };
|
||||
}
|
||||
if (argv.includes("--list")) {
|
||||
return { kind: "list" };
|
||||
}
|
||||
const index = argv.indexOf("--lane");
|
||||
if (index === -1) {
|
||||
throw new Error("--lane is required");
|
||||
}
|
||||
const laneName = argv[index + 1];
|
||||
if (!laneName || laneName.startsWith("-")) {
|
||||
throw new Error("--lane requires a value");
|
||||
}
|
||||
return { kind: "run", laneName };
|
||||
}
|
||||
|
||||
export function resolveQaDockerE2eLane(
|
||||
laneName: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): ResolvedQaDockerE2eLane {
|
||||
if (!isQaDockerE2eLaneName(laneName)) {
|
||||
throw new Error(`unknown Docker E2E lane: ${laneName}\n\n${formatQaDockerE2eLaneUsage()}`);
|
||||
}
|
||||
const lane = QA_DOCKER_E2E_LANES[laneName];
|
||||
return {
|
||||
env: { ...env, ...lane.env?.(env) },
|
||||
name: laneName,
|
||||
script: lane.script,
|
||||
};
|
||||
}
|
||||
|
||||
export function runQaDockerE2eLane(
|
||||
laneName: string,
|
||||
deps: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
spawn?: SpawnQaDockerE2eLane;
|
||||
} = {},
|
||||
): QaDockerE2eLaneRunResult {
|
||||
const lane = resolveQaDockerE2eLane(laneName, deps.env);
|
||||
const spawn = deps.spawn ?? spawnSync;
|
||||
return spawn("bash", [lane.script], {
|
||||
env: lane.env,
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
function isQaDockerE2eLaneName(laneName: string): laneName is QaDockerE2eLaneName {
|
||||
return Object.hasOwn(QA_DOCKER_E2E_LANES, laneName);
|
||||
}
|
||||
28
test/e2e/qa-lab/runtime/docker-e2e-lane.ts
Normal file
28
test/e2e/qa-lab/runtime/docker-e2e-lane.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Runs an existing Docker E2E lane through the QA Lab script scenario contract.
|
||||
import {
|
||||
formatQaDockerE2eLaneUsage,
|
||||
listQaDockerE2eLaneNames,
|
||||
parseQaDockerE2eLaneArgs,
|
||||
runQaDockerE2eLane,
|
||||
} from "./docker-e2e-lane.fixture.ts";
|
||||
|
||||
const args = parseQaDockerE2eLaneArgs(process.argv.slice(2));
|
||||
if (args.kind === "help") {
|
||||
console.log(formatQaDockerE2eLaneUsage());
|
||||
process.exit(0);
|
||||
}
|
||||
if (args.kind === "list") {
|
||||
console.log(listQaDockerE2eLaneNames().join("\n"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const result = runQaDockerE2eLane(args.laneName);
|
||||
|
||||
if (result.error) {
|
||||
console.error(result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
if (result.signal) {
|
||||
process.kill(process.pid, result.signal);
|
||||
}
|
||||
process.exit(result.status ?? 1);
|
||||
Loading…
Add table
Add a link
Reference in a new issue