mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
improve(qa): consolidate live transport selector contracts (#108465)
* refactor(qa): consolidate live transport selectors * refactor(qa): remove live transport indirection
This commit is contained in:
@@ -3,21 +3,17 @@ import { Command } from "commander";
|
||||
import type { QaRunnerCliContribution } from "openclaw/plugin-sdk/qa-runner-runtime";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { listQaRunnerCliContributions, runDiscord, runSlack, runTelegram, runWhatsApp } = vi.hoisted(
|
||||
const { listQaRunnerCliContributions, runLiveTransportQaSuiteCommand, runTelegram } = vi.hoisted(
|
||||
() => ({
|
||||
listQaRunnerCliContributions: vi.fn<() => QaRunnerCliContribution[]>(() => []),
|
||||
runDiscord: vi.fn(),
|
||||
runSlack: vi.fn(),
|
||||
runLiveTransportQaSuiteCommand: vi.fn(),
|
||||
runTelegram: vi.fn(),
|
||||
runWhatsApp: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/qa-runner-runtime", () => ({ listQaRunnerCliContributions }));
|
||||
vi.mock("./discord/cli.runtime.js", () => ({ runQaDiscordCommand: runDiscord }));
|
||||
vi.mock("./slack/cli.runtime.js", () => ({ runQaSlackCommand: runSlack }));
|
||||
vi.mock("./shared/live-transport-suite.runtime.js", () => ({ runLiveTransportQaSuiteCommand }));
|
||||
vi.mock("./telegram/cli.runtime.js", () => ({ runQaTelegramCommand: runTelegram }));
|
||||
vi.mock("./whatsapp/cli.runtime.js", () => ({ runQaWhatsAppCommand: runWhatsApp }));
|
||||
|
||||
import { listLiveTransportQaAdapterFactories, listLiveTransportQaCliRegistrations } from "./cli.js";
|
||||
|
||||
@@ -37,22 +33,37 @@ describe("live transport QA contributions", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["telegram", runTelegram],
|
||||
["discord", runDiscord],
|
||||
["slack", runSlack],
|
||||
["whatsapp", runWhatsApp],
|
||||
] as const)("keeps the shipped %s command runner", async (commandName, runCommand) => {
|
||||
it.each(["discord", "slack", "whatsapp"] as const)(
|
||||
"routes the shipped %s command through the shared suite host",
|
||||
async (commandName) => {
|
||||
const registration = listLiveTransportQaCliRegistrations().find(
|
||||
(candidate) => candidate.commandName === commandName,
|
||||
);
|
||||
const qa = new Command();
|
||||
registration?.register(qa);
|
||||
|
||||
await qa.parseAsync(["node", "openclaw", commandName, "--scenario", `${commandName}-canary`]);
|
||||
|
||||
expect(runLiveTransportQaSuiteCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channelId: commandName,
|
||||
options: expect.objectContaining({ scenarioIds: [`${commandName}-canary`] }),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the specialized Telegram command runner", async () => {
|
||||
const registration = listLiveTransportQaCliRegistrations().find(
|
||||
(candidate) => candidate.commandName === commandName,
|
||||
(candidate) => candidate.commandName === "telegram",
|
||||
);
|
||||
const qa = new Command();
|
||||
registration?.register(qa);
|
||||
|
||||
await qa.parseAsync(["node", "openclaw", commandName, "--scenario", `${commandName}-canary`]);
|
||||
await qa.parseAsync(["node", "openclaw", "telegram", "--scenario", "telegram-canary"]);
|
||||
|
||||
expect(runCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ scenarioIds: [`${commandName}-canary`] }),
|
||||
expect(runTelegram).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ scenarioIds: ["telegram-canary"] }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// QA Lab Discord tests cover CLI delegation into the shared suite host.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runQaSuiteCommand = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../cli.runtime.js", () => ({ runQaSuiteCommand }));
|
||||
vi.mock("../shared/live-transport-cli.runtime.js", () => ({
|
||||
resolveLiveTransportQaRunOptions: (opts: Record<string, unknown>) => ({
|
||||
...opts,
|
||||
repoRoot: "/resolved-repo",
|
||||
outputDir: "/resolved-repo/.artifacts/resolved",
|
||||
providerMode: opts.providerMode ?? "mock-openai",
|
||||
}),
|
||||
}));
|
||||
|
||||
import { listQaScenariosForExecutionProfile } from "../../scenario-catalog.js";
|
||||
import { runQaDiscordCommand } from "./cli.runtime.js";
|
||||
|
||||
describe("QA Lab Discord CLI runtime", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("delegates the default profile into the Discord adapter host", async () => {
|
||||
await runQaDiscordCommand({
|
||||
repoRoot: "/repo",
|
||||
outputDir: ".artifacts/discord",
|
||||
providerMode: "mock-openai",
|
||||
primaryModel: "mock-openai/gpt-5.5",
|
||||
alternateModel: "mock-openai/gpt-5.5-alt",
|
||||
fastMode: true,
|
||||
allowFailures: true,
|
||||
failFast: true,
|
||||
credentialSource: "convex",
|
||||
credentialRole: "ci",
|
||||
sutAccountId: "discord-sut",
|
||||
});
|
||||
|
||||
expect(runQaSuiteCommand).toHaveBeenCalledWith({
|
||||
repoRoot: "/repo",
|
||||
outputDir: ".artifacts/discord",
|
||||
providerMode: "mock-openai",
|
||||
primaryModel: "mock-openai/gpt-5.5",
|
||||
alternateModel: "mock-openai/gpt-5.5-alt",
|
||||
fastMode: true,
|
||||
allowFailures: true,
|
||||
failFast: true,
|
||||
channelDriver: "live",
|
||||
channel: "discord",
|
||||
concurrency: 1,
|
||||
scenarioIds: listQaScenariosForExecutionProfile("discord:adapter").map(
|
||||
(scenario) => scenario.id,
|
||||
),
|
||||
sutAccountId: "discord-sut",
|
||||
credentialSource: "convex",
|
||||
credentialRole: "ci",
|
||||
explicitScenarioSelection: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("lets explicit scenarios override profile selection", async () => {
|
||||
await runQaDiscordCommand({ scenarioIds: ["discord-voice-autojoin"] });
|
||||
|
||||
expect(runQaSuiteCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
explicitScenarioSelection: true,
|
||||
scenarioIds: ["discord-voice-autojoin"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { runQaSuiteCommand } from "../../cli.runtime.js";
|
||||
import type { LiveTransportQaCommandOptions } from "../shared/live-transport-cli.js";
|
||||
import { resolveLiveTransportQaRunOptions } from "../shared/live-transport-cli.runtime.js";
|
||||
import { resolveDiscordQaScenarioIds } from "./profiles.js";
|
||||
|
||||
export async function runQaDiscordCommand(opts: LiveTransportQaCommandOptions) {
|
||||
const runOptions = resolveLiveTransportQaRunOptions(opts);
|
||||
return await runQaSuiteCommand({
|
||||
repoRoot: opts.repoRoot,
|
||||
outputDir: opts.outputDir,
|
||||
providerMode: runOptions.providerMode,
|
||||
primaryModel: runOptions.primaryModel,
|
||||
alternateModel: runOptions.alternateModel,
|
||||
fastMode: runOptions.fastMode,
|
||||
allowFailures: runOptions.allowFailures,
|
||||
failFast: runOptions.failFast,
|
||||
channelDriver: "live",
|
||||
channel: "discord",
|
||||
concurrency: 1,
|
||||
scenarioIds: resolveDiscordQaScenarioIds(runOptions.scenarioIds),
|
||||
sutAccountId: runOptions.sutAccountId,
|
||||
credentialSource: runOptions.credentialSource,
|
||||
credentialRole: runOptions.credentialRole,
|
||||
explicitScenarioSelection: Boolean(runOptions.scenarioIds?.length),
|
||||
});
|
||||
}
|
||||
@@ -1,36 +1,37 @@
|
||||
// Qa Lab plugin module implements cli behavior.
|
||||
import {
|
||||
createLiveTransportQaAdapterFactory,
|
||||
createLazyCliRuntimeLoader,
|
||||
createLiveTransportQaCliRegistration,
|
||||
loadLiveTransportQaSuiteRuntime,
|
||||
type LiveTransportQaCliRegistration,
|
||||
type LiveTransportQaCommandOptions,
|
||||
} from "../shared/live-transport-cli.js";
|
||||
import { resolveDiscordQaScenarioIds } from "./scenario-selection.js";
|
||||
|
||||
type DiscordQaCliRuntime = typeof import("./cli.runtime.js");
|
||||
type DiscordQaAdapterRuntime = typeof import("./adapter.runtime.js");
|
||||
|
||||
const loadDiscordQaCliRuntime = createLazyCliRuntimeLoader<DiscordQaCliRuntime>(
|
||||
() => import("./cli.runtime.js"),
|
||||
);
|
||||
const loadDiscordQaAdapterRuntime = createLazyCliRuntimeLoader<DiscordQaAdapterRuntime>(
|
||||
() => import("./adapter.runtime.js"),
|
||||
);
|
||||
const loadDiscordQaAdapterRuntime = createLazyCliRuntimeLoader<
|
||||
typeof import("./adapter.runtime.js")
|
||||
>(() => import("./adapter.runtime.js"));
|
||||
|
||||
async function runQaDiscord(opts: LiveTransportQaCommandOptions) {
|
||||
const runtime = await loadDiscordQaCliRuntime();
|
||||
await runtime.runQaDiscordCommand(opts);
|
||||
const runtime = await loadLiveTransportQaSuiteRuntime();
|
||||
await runtime.runLiveTransportQaSuiteCommand({
|
||||
channelId: "discord",
|
||||
defaultProviderMode: "live-frontier",
|
||||
options: opts,
|
||||
selectScenarioIds: resolveDiscordQaScenarioIds,
|
||||
});
|
||||
}
|
||||
|
||||
export const discordQaCliRegistration: LiveTransportQaCliRegistration =
|
||||
createLiveTransportQaCliRegistration({
|
||||
commandName: "discord",
|
||||
adapterFactory: {
|
||||
adapterFactory: createLiveTransportQaAdapterFactory({
|
||||
id: "discord",
|
||||
matches: ({ channelId, driver }) => driver === "live" && channelId === "discord",
|
||||
async create(context) {
|
||||
return await (await loadDiscordQaAdapterRuntime()).createDiscordQaTransportAdapter(context);
|
||||
return (await loadDiscordQaAdapterRuntime()).createDiscordQaTransportAdapter(context);
|
||||
},
|
||||
},
|
||||
}),
|
||||
credentialOptions: {
|
||||
sourceDescription: "Credential source for Discord QA: env or convex (default: env)",
|
||||
roleDescription:
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readQaScenarioPack } from "../../scenario-catalog.js";
|
||||
import * as scenarioRuntime from "./scenario-runtime.js";
|
||||
|
||||
describe("legacy Discord scenario migration", () => {
|
||||
it("keeps every retired runner id as a Discord module scenario", () => {
|
||||
const scenarios = readQaScenarioPack().scenarios.filter(
|
||||
(scenario) =>
|
||||
scenario.execution.kind === "flow" &&
|
||||
scenario.execution.channel === "discord" &&
|
||||
JSON.stringify(scenario.execution.flow).includes(
|
||||
"./live-transports/discord/scenario-runtime.js",
|
||||
),
|
||||
);
|
||||
expect(scenarios).toHaveLength(6);
|
||||
for (const scenario of scenarios) {
|
||||
expect(scenario.execution).toMatchObject({ kind: "flow", channel: "discord" });
|
||||
expect(JSON.stringify(scenario.execution.flow)).toContain(
|
||||
"./live-transports/discord/scenario-runtime.js",
|
||||
);
|
||||
const flowText = JSON.stringify(scenario.execution.flow);
|
||||
const callName = flowText.match(/scenarioModule\.([A-Za-z0-9]+)/u)?.[1];
|
||||
expect(typeof scenarioRuntime[callName as keyof typeof scenarioRuntime], scenario.id).toBe(
|
||||
"function",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { listQaScenariosForExecutionProfile } from "../../scenario-catalog.js";
|
||||
|
||||
export function resolveDiscordQaScenarioIds(scenarioIds?: readonly string[]) {
|
||||
export function resolveDiscordQaScenarioIds({ scenarioIds }: { scenarioIds?: readonly string[] }) {
|
||||
return scenarioIds?.length
|
||||
? [...scenarioIds]
|
||||
: listQaScenariosForExecutionProfile("discord:adapter").map((scenario) => scenario.id);
|
||||
@@ -5,12 +5,18 @@ import { createQaChannelTransport } from "../qa-channel-transport.js";
|
||||
import { createQaTransportAdapter } from "../qa-transport-registry.js";
|
||||
import { listQaScenariosForExecutionProfile } from "../scenario-catalog.js";
|
||||
|
||||
const { createSlack, createTelegram, createWhatsApp } = vi.hoisted(() => ({
|
||||
createSlack: vi.fn(),
|
||||
createTelegram: vi.fn(),
|
||||
createWhatsApp: vi.fn(),
|
||||
}));
|
||||
const { createDiscord, createMatrix, createSlack, createTelegram, createWhatsApp } = vi.hoisted(
|
||||
() => ({
|
||||
createDiscord: vi.fn(),
|
||||
createMatrix: vi.fn(),
|
||||
createSlack: vi.fn(),
|
||||
createTelegram: vi.fn(),
|
||||
createWhatsApp: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("./discord/adapter.runtime.js", () => ({ createDiscordQaTransportAdapter: createDiscord }));
|
||||
vi.mock("./matrix/adapter.runtime.js", () => ({ createMatrixQaTransportAdapter: createMatrix }));
|
||||
vi.mock("./slack/adapter.runtime.js", () => ({ createSlackQaTransportAdapter: createSlack }));
|
||||
vi.mock("./telegram/adapter.runtime.js", () => ({
|
||||
createTelegramQaTransportAdapter: createTelegram,
|
||||
@@ -19,19 +25,31 @@ vi.mock("./whatsapp/adapter.runtime.js", () => ({
|
||||
createWhatsAppQaTransportAdapter: createWhatsApp,
|
||||
}));
|
||||
|
||||
import { discordQaCliRegistration } from "./discord/cli.js";
|
||||
import { matrixQaCliRegistration } from "./matrix/cli.js";
|
||||
import { slackQaCliRegistration } from "./slack/cli.js";
|
||||
import { telegramQaCliRegistration } from "./telegram/cli.js";
|
||||
import { whatsappQaCliRegistration } from "./whatsapp/cli.js";
|
||||
|
||||
const discordQaAdapterFactory = discordQaCliRegistration.adapterFactory;
|
||||
const matrixQaAdapterFactory = matrixQaCliRegistration.adapterFactory;
|
||||
const slackQaAdapterFactory = slackQaCliRegistration.adapterFactory;
|
||||
const telegramQaAdapterFactory = telegramQaCliRegistration.adapterFactory;
|
||||
const whatsappQaAdapterFactory = whatsappQaCliRegistration.adapterFactory;
|
||||
if (!slackQaAdapterFactory || !telegramQaAdapterFactory || !whatsappQaAdapterFactory) {
|
||||
if (
|
||||
!discordQaAdapterFactory ||
|
||||
!matrixQaAdapterFactory ||
|
||||
!slackQaAdapterFactory ||
|
||||
!telegramQaAdapterFactory ||
|
||||
!whatsappQaAdapterFactory
|
||||
) {
|
||||
throw new Error("expected live transport adapter factories");
|
||||
}
|
||||
|
||||
const factories = [
|
||||
telegramQaAdapterFactory,
|
||||
discordQaAdapterFactory,
|
||||
matrixQaAdapterFactory,
|
||||
slackQaAdapterFactory,
|
||||
whatsappQaAdapterFactory,
|
||||
] as const;
|
||||
@@ -64,6 +82,8 @@ describe("live transport adapter factories", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
["discord", createDiscord],
|
||||
["matrix", createMatrix],
|
||||
["telegram", createTelegram],
|
||||
["slack", createSlack],
|
||||
["whatsapp", createWhatsApp],
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
// QA Lab Matrix tests cover CLI delegation into the shared suite host.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runQaSuiteCommand = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../cli.runtime.js", () => ({ runQaSuiteCommand }));
|
||||
|
||||
import { runQaMatrixCommand } from "./cli.runtime.js";
|
||||
import { resolveMatrixQaScenarioIds } from "./profiles.js";
|
||||
|
||||
describe("QA Lab Matrix CLI runtime", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("delegates the release profile into the live Matrix adapter host", async () => {
|
||||
await runQaMatrixCommand({
|
||||
repoRoot: "/repo",
|
||||
outputDir: ".artifacts/matrix",
|
||||
providerMode: "mock-openai",
|
||||
primaryModel: "mock-openai/gpt-5.5",
|
||||
alternateModel: "mock-openai/gpt-5.5-alt",
|
||||
fastMode: true,
|
||||
failFast: true,
|
||||
profile: "release",
|
||||
sutAccountId: "matrix-sut",
|
||||
});
|
||||
|
||||
expect(runQaSuiteCommand).toHaveBeenCalledWith({
|
||||
repoRoot: "/repo",
|
||||
outputDir: ".artifacts/matrix",
|
||||
providerMode: "mock-openai",
|
||||
primaryModel: "mock-openai/gpt-5.5",
|
||||
alternateModel: "mock-openai/gpt-5.5-alt",
|
||||
fastMode: true,
|
||||
allowFailures: undefined,
|
||||
failFast: true,
|
||||
channelDriver: "live",
|
||||
channel: "matrix",
|
||||
concurrency: 1,
|
||||
scenarioIds: ["channel-chat-baseline", "matrix-allowlist-hot-reload"],
|
||||
sutAccountId: "matrix-sut",
|
||||
});
|
||||
});
|
||||
|
||||
it("lets explicit scenarios override profile selection", async () => {
|
||||
await runQaMatrixCommand({
|
||||
profile: "all",
|
||||
scenarioIds: ["matrix-restart-resume"],
|
||||
});
|
||||
|
||||
expect(runQaSuiteCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ scenarioIds: ["matrix-restart-resume"] }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the dedicated Matrix command default on all profile scenarios", async () => {
|
||||
await runQaMatrixCommand({});
|
||||
|
||||
const call = runQaSuiteCommand.mock.calls.at(-1)?.[0];
|
||||
expect(call?.scenarioIds).toEqual(resolveMatrixQaScenarioIds({}));
|
||||
expect(call?.scenarioIds).toContain("matrix-e2ee-cli-encryption-setup");
|
||||
});
|
||||
|
||||
it("delegates restored E2EE profiles through the same live Matrix adapter host", async () => {
|
||||
await runQaMatrixCommand({ profile: "e2ee-deep" });
|
||||
|
||||
expect(runQaSuiteCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "matrix",
|
||||
channelDriver: "live",
|
||||
scenarioIds: expect.arrayContaining([
|
||||
"matrix-e2ee-state-loss-external-recovery-key",
|
||||
"matrix-e2ee-server-device-deleted-relogin-recovers",
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects unknown provider modes before suite dispatch", async () => {
|
||||
await expect(runQaMatrixCommand({ providerMode: "unknown" })).rejects.toThrow(
|
||||
"unknown QA provider mode: unknown",
|
||||
);
|
||||
expect(runQaSuiteCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects shared credential leases", async () => {
|
||||
await expect(runQaMatrixCommand({ credentialSource: "convex" })).rejects.toThrow(
|
||||
"supports only --credential-source env",
|
||||
);
|
||||
expect(runQaSuiteCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
// QA Lab Matrix delegates CLI profile selection into the shared suite host.
|
||||
import { runQaSuiteCommand } from "../../cli.runtime.js";
|
||||
import { normalizeQaProviderMode } from "../../run-config.js";
|
||||
import type { LiveTransportQaCommandOptions } from "../shared/live-transport-cli.js";
|
||||
import { resolveMatrixQaScenarioIds } from "./profiles.js";
|
||||
|
||||
export async function runQaMatrixCommand(opts: LiveTransportQaCommandOptions) {
|
||||
const credentialSource = opts.credentialSource?.trim().toLowerCase();
|
||||
if (credentialSource && credentialSource !== "env") {
|
||||
throw new Error(
|
||||
"QA Lab Matrix supports only --credential-source env because its homeserver is disposable and local.",
|
||||
);
|
||||
}
|
||||
if (opts.credentialRole?.trim()) {
|
||||
throw new Error("QA Lab Matrix does not use credential roles.");
|
||||
}
|
||||
|
||||
return await runQaSuiteCommand({
|
||||
repoRoot: opts.repoRoot,
|
||||
outputDir: opts.outputDir,
|
||||
providerMode:
|
||||
opts.providerMode === undefined ? undefined : normalizeQaProviderMode(opts.providerMode),
|
||||
primaryModel: opts.primaryModel,
|
||||
alternateModel: opts.alternateModel,
|
||||
fastMode: opts.fastMode,
|
||||
allowFailures: opts.allowFailures,
|
||||
failFast: opts.failFast,
|
||||
channelDriver: "live",
|
||||
channel: "matrix",
|
||||
concurrency: 1,
|
||||
scenarioIds: resolveMatrixQaScenarioIds({
|
||||
profile: opts.profile,
|
||||
scenarioIds: opts.scenarioIds,
|
||||
}),
|
||||
sutAccountId: opts.sutAccountId,
|
||||
});
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
import { Command } from "commander";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runQaMatrixCommand = vi.hoisted(() => vi.fn());
|
||||
const runLiveTransportQaSuiteCommand = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./cli.runtime.js", () => ({ runQaMatrixCommand }));
|
||||
vi.mock("../shared/live-transport-suite.runtime.js", () => ({ runLiveTransportQaSuiteCommand }));
|
||||
|
||||
import { matrixQaCliRegistration } from "./cli.js";
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("QA Lab Matrix CLI registration", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
process.exitCode = undefined;
|
||||
runQaMatrixCommand.mockReset();
|
||||
runLiveTransportQaSuiteCommand.mockReset();
|
||||
exitSpy = vi.spyOn(process, "exit").mockImplementation((code?: string | number | null) => {
|
||||
throw new Error(`process.exit(${String(code)})`);
|
||||
});
|
||||
@@ -89,11 +89,15 @@ describe("QA Lab Matrix CLI registration", () => {
|
||||
"matrix-allowlist-hot-reload",
|
||||
]);
|
||||
|
||||
expect(runQaMatrixCommand).toHaveBeenCalledWith(
|
||||
expect(runLiveTransportQaSuiteCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
profile: "release",
|
||||
providerMode: "live-frontier",
|
||||
scenarioIds: ["matrix-allowlist-hot-reload"],
|
||||
channelId: "matrix",
|
||||
credentialMode: "env-only",
|
||||
options: expect.objectContaining({
|
||||
profile: "release",
|
||||
providerMode: "live-frontier",
|
||||
scenarioIds: ["matrix-allowlist-hot-reload"],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -101,7 +105,7 @@ describe("QA Lab Matrix CLI registration", () => {
|
||||
it("exits successfully after Matrix artifacts are written", async () => {
|
||||
const qa = new Command();
|
||||
matrixQaCliRegistration.register(qa);
|
||||
runQaMatrixCommand.mockResolvedValue(undefined);
|
||||
runLiveTransportQaSuiteCommand.mockResolvedValue(undefined);
|
||||
|
||||
await expect(qa.parseAsync(["node", "openclaw", "matrix"])).rejects.toThrow("process.exit(0)");
|
||||
|
||||
@@ -111,7 +115,9 @@ describe("QA Lab Matrix CLI registration", () => {
|
||||
it("prints a failed run and exits after its artifacts are written", async () => {
|
||||
const qa = new Command();
|
||||
matrixQaCliRegistration.register(qa);
|
||||
runQaMatrixCommand.mockRejectedValue(new Error("Matrix QA failed.\nreport: /tmp/report.md"));
|
||||
runLiveTransportQaSuiteCommand.mockRejectedValue(
|
||||
new Error("Matrix QA failed.\nreport: /tmp/report.md"),
|
||||
);
|
||||
|
||||
await expect(qa.parseAsync(["node", "openclaw", "matrix"])).rejects.toThrow("process.exit(1)");
|
||||
|
||||
@@ -122,7 +128,7 @@ describe("QA Lab Matrix CLI registration", () => {
|
||||
it("preserves a failed suite exit code after the runtime returns", async () => {
|
||||
const qa = new Command();
|
||||
matrixQaCliRegistration.register(qa);
|
||||
runQaMatrixCommand.mockImplementation(async () => {
|
||||
runLiveTransportQaSuiteCommand.mockImplementation(async () => {
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
@@ -135,7 +141,7 @@ describe("QA Lab Matrix CLI registration", () => {
|
||||
process.env.OPENCLAW_QA_MATRIX_DISABLE_FORCE_EXIT = "1";
|
||||
const qa = new Command();
|
||||
matrixQaCliRegistration.register(qa);
|
||||
runQaMatrixCommand.mockRejectedValue(new Error("scenario failed"));
|
||||
runLiveTransportQaSuiteCommand.mockRejectedValue(new Error("scenario failed"));
|
||||
|
||||
await expect(qa.parseAsync(["node", "openclaw", "matrix"])).rejects.toThrow("scenario failed");
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// Qa Lab plugin module implements Matrix live transport CLI behavior.
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import {
|
||||
createLiveTransportQaAdapterFactory,
|
||||
createLazyCliRuntimeLoader,
|
||||
createLiveTransportQaCliRegistration,
|
||||
loadLiveTransportQaSuiteRuntime,
|
||||
type LiveTransportQaCliRegistration,
|
||||
type LiveTransportQaCommandOptions,
|
||||
} from "../shared/live-transport-cli.js";
|
||||
import { resolveMatrixQaScenarioIds } from "./profiles.js";
|
||||
|
||||
const DISABLE_MATRIX_QA_FORCE_EXIT_ENV = "OPENCLAW_QA_MATRIX_DISABLE_FORCE_EXIT";
|
||||
|
||||
const loadMatrixQaCliRuntime = createLazyCliRuntimeLoader<typeof import("./cli.runtime.js")>(
|
||||
() => import("./cli.runtime.js"),
|
||||
);
|
||||
const loadMatrixQaAdapterRuntime = createLazyCliRuntimeLoader<
|
||||
typeof import("./adapter.runtime.js")
|
||||
>(() => import("./adapter.runtime.js"));
|
||||
@@ -37,7 +37,18 @@ async function exitMatrixQaCommand(code: number): Promise<never> {
|
||||
}
|
||||
|
||||
async function runQaMatrix(opts: LiveTransportQaCommandOptions) {
|
||||
const run = async () => await (await loadMatrixQaCliRuntime()).runQaMatrixCommand(opts);
|
||||
const run = async () => {
|
||||
const runtime = await loadLiveTransportQaSuiteRuntime();
|
||||
await runtime.runLiveTransportQaSuiteCommand({
|
||||
channelId: "matrix",
|
||||
credentialMode: "env-only",
|
||||
defaultProviderMode: "live-frontier",
|
||||
envCredentialReason: "its homeserver is disposable and local.",
|
||||
laneLabel: "Matrix",
|
||||
options: opts,
|
||||
selectScenarioIds: resolveMatrixQaScenarioIds,
|
||||
});
|
||||
};
|
||||
if (process.env[DISABLE_MATRIX_QA_FORCE_EXIT_ENV] === "1") {
|
||||
await run();
|
||||
return;
|
||||
@@ -54,18 +65,15 @@ async function runQaMatrix(opts: LiveTransportQaCommandOptions) {
|
||||
await exitMatrixQaCommand(exitCode);
|
||||
}
|
||||
|
||||
const matrixQaAdapterFactory: NonNullable<LiveTransportQaCliRegistration["adapterFactory"]> = {
|
||||
id: "matrix",
|
||||
matches: ({ channelId, driver }) => driver === "live" && channelId === "matrix",
|
||||
async create(context) {
|
||||
return await (await loadMatrixQaAdapterRuntime()).createMatrixQaTransportAdapter(context);
|
||||
},
|
||||
};
|
||||
|
||||
export const matrixQaCliRegistration: LiveTransportQaCliRegistration =
|
||||
createLiveTransportQaCliRegistration({
|
||||
commandName: "matrix",
|
||||
adapterFactory: matrixQaAdapterFactory,
|
||||
adapterFactory: createLiveTransportQaAdapterFactory({
|
||||
id: "matrix",
|
||||
async create(context) {
|
||||
return (await loadMatrixQaAdapterRuntime()).createMatrixQaTransportAdapter(context);
|
||||
},
|
||||
}),
|
||||
defaultProviderMode: "live-frontier",
|
||||
description: "Run the Docker-backed Matrix live QA lane against a disposable homeserver",
|
||||
outputDirHelp: "Matrix QA artifact directory",
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readQaScenarioById } from "../../scenario-catalog.js";
|
||||
import { resolveMatrixQaScenarioIds } from "./profiles.js";
|
||||
|
||||
const MATRIX_QA_PROFILE_NAMES = [
|
||||
"all",
|
||||
"fast",
|
||||
"release",
|
||||
"transport",
|
||||
"media",
|
||||
"e2ee-smoke",
|
||||
"e2ee-deep",
|
||||
"e2ee-cli",
|
||||
] as const;
|
||||
|
||||
describe("QA Lab Matrix profiles", () => {
|
||||
it("preserves the legacy profile sizes and default selection", () => {
|
||||
it("preserves the profile sizes and default selection", () => {
|
||||
const allScenarioIds = resolveMatrixQaScenarioIds({ profile: "all" });
|
||||
expect(allScenarioIds).toHaveLength(93);
|
||||
expect(resolveMatrixQaScenarioIds({ profile: "fast" })).toHaveLength(12);
|
||||
@@ -20,22 +32,27 @@ describe("QA Lab Matrix profiles", () => {
|
||||
expect(allScenarioIds).toContain("channel-chat-baseline");
|
||||
});
|
||||
|
||||
it("keeps profile ids unique and excludes the legacy explicit-only scenarios", () => {
|
||||
for (const profile of [
|
||||
"all",
|
||||
"fast",
|
||||
"release",
|
||||
"transport",
|
||||
"media",
|
||||
"e2ee-smoke",
|
||||
"e2ee-deep",
|
||||
"e2ee-cli",
|
||||
]) {
|
||||
it("keeps every profile unique, catalog-backed, and contained by all", () => {
|
||||
const allScenarioIds = resolveMatrixQaScenarioIds({ profile: "all" });
|
||||
const allScenarioIdSet = new Set(allScenarioIds);
|
||||
for (const profile of MATRIX_QA_PROFILE_NAMES) {
|
||||
const scenarioIds = resolveMatrixQaScenarioIds({ profile });
|
||||
expect(new Set(scenarioIds).size).toBe(scenarioIds.length);
|
||||
for (const scenarioId of scenarioIds) {
|
||||
expect(readQaScenarioById(scenarioId).id).toBe(scenarioId);
|
||||
if (profile !== "all") {
|
||||
expect(allScenarioIdSet.has(scenarioId), `${profile}:${scenarioId}`).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
const allScenarioIds = resolveMatrixQaScenarioIds({ profile: "all" });
|
||||
expect(allScenarioIds).not.toContain("matrix-room-block-streaming");
|
||||
expect(allScenarioIds).not.toContain("subagent-thread-spawn");
|
||||
});
|
||||
|
||||
it("keeps explicit-only scenarios out of every named profile", () => {
|
||||
const profileScenarioIds = MATRIX_QA_PROFILE_NAMES.flatMap((profile) =>
|
||||
resolveMatrixQaScenarioIds({ profile }),
|
||||
);
|
||||
|
||||
expect(profileScenarioIds).not.toContain("matrix-room-block-streaming");
|
||||
expect(profileScenarioIds).not.toContain("subagent-thread-spawn");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readQaScenarioPack, type QaSeedScenarioWithSource } from "../scenario-catalog.js";
|
||||
import * as discordScenarioRuntime from "./discord/scenario-runtime.js";
|
||||
import * as slackScenarioRuntime from "./slack/scenario-runtime.js";
|
||||
import * as whatsappScenarioRuntime from "./whatsapp/scenario-runtime.js";
|
||||
|
||||
const LANES = [
|
||||
{
|
||||
channel: "discord",
|
||||
modulePath: "./live-transports/discord/scenario-runtime.js",
|
||||
runtime: discordScenarioRuntime,
|
||||
},
|
||||
{
|
||||
channel: "slack",
|
||||
modulePath: "./live-transports/slack/scenario-runtime.js",
|
||||
runtime: slackScenarioRuntime,
|
||||
},
|
||||
{
|
||||
channel: "whatsapp",
|
||||
modulePath: "./live-transports/whatsapp/scenario-runtime.js",
|
||||
runtime: whatsappScenarioRuntime,
|
||||
},
|
||||
] as const;
|
||||
|
||||
function readScenarioModuleCallName(
|
||||
scenario: QaSeedScenarioWithSource,
|
||||
modulePath: string,
|
||||
): string | undefined {
|
||||
if (scenario.execution.kind !== "flow" || !scenario.execution.flow) {
|
||||
return undefined;
|
||||
}
|
||||
const actions = scenario.execution.flow.steps.flatMap((step) => step.actions);
|
||||
const importExpression = `await qaImport(${JSON.stringify(modulePath)})`;
|
||||
const importsModule = actions.some(
|
||||
(action) =>
|
||||
typeof action === "object" &&
|
||||
action !== null &&
|
||||
"set" in action &&
|
||||
action.set === "scenarioModule" &&
|
||||
"value" in action &&
|
||||
typeof action.value === "object" &&
|
||||
action.value !== null &&
|
||||
"expr" in action.value &&
|
||||
action.value.expr === importExpression,
|
||||
);
|
||||
if (!importsModule) {
|
||||
return undefined;
|
||||
}
|
||||
const callPrefix = "scenarioModule.";
|
||||
const callAction = actions.find(
|
||||
(action): action is { call: string } =>
|
||||
typeof action === "object" &&
|
||||
action !== null &&
|
||||
"call" in action &&
|
||||
typeof action.call === "string" &&
|
||||
action.call.startsWith(callPrefix),
|
||||
);
|
||||
if (!callAction) {
|
||||
throw new Error(`scenario module flow has no call: ${scenario.id}`);
|
||||
}
|
||||
return callAction.call.slice(callPrefix.length);
|
||||
}
|
||||
|
||||
describe("live transport scenario module parity", () => {
|
||||
it.each(LANES)(
|
||||
"keeps $channel scenario definitions and runtime exports in one-to-one parity",
|
||||
({ channel, modulePath, runtime }) => {
|
||||
const bindings = readQaScenarioPack().scenarios.flatMap((scenario) => {
|
||||
if (scenario.execution.kind !== "flow" || scenario.execution.channel !== channel) {
|
||||
return [];
|
||||
}
|
||||
const callName = readScenarioModuleCallName(scenario, modulePath);
|
||||
return callName ? [{ callName, scenarioId: scenario.id }] : [];
|
||||
});
|
||||
const callNames = bindings.map(({ callName, scenarioId }) => {
|
||||
expect(Reflect.get(runtime, callName), scenarioId).toBeTypeOf("function");
|
||||
return callName;
|
||||
});
|
||||
|
||||
expect(new Set(callNames).size).toBe(callNames.length);
|
||||
expect(callNames.toSorted()).toEqual(Object.keys(runtime).toSorted());
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
type QaInferredCredentialSource = "convex" | "env";
|
||||
|
||||
export function inferQaCredentialSource(
|
||||
value: string | undefined,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): QaInferredCredentialSource {
|
||||
const normalized =
|
||||
value?.trim().toLowerCase() || env.OPENCLAW_QA_CREDENTIAL_SOURCE?.trim().toLowerCase();
|
||||
return normalized === "convex" ? "convex" : "env";
|
||||
}
|
||||
@@ -139,6 +139,11 @@ function createSharedLiveTransportQaCliRegistration(
|
||||
};
|
||||
}
|
||||
|
||||
// All dedicated commands share one memoized import of the consolidated suite host.
|
||||
export const loadLiveTransportQaSuiteRuntime = createLazyCliRuntimeLoader<
|
||||
typeof import("./live-transport-suite.runtime.js")
|
||||
>(() => import("./live-transport-suite.runtime.js"));
|
||||
|
||||
type QaLabLiveTransportQaCliRegistrationOptions = Omit<
|
||||
LiveTransportQaCliRegistrationOptions,
|
||||
"allowFailuresHelp" | "defaultProviderMode" | "providerModeHelp"
|
||||
@@ -156,3 +161,14 @@ export function createLiveTransportQaCliRegistration(
|
||||
providerModeHelp: formatQaProviderModeHelp(),
|
||||
});
|
||||
}
|
||||
|
||||
export function createLiveTransportQaAdapterFactory(params: {
|
||||
create: NonNullable<LiveTransportQaCliRegistrationOptions["adapterFactory"]>["create"];
|
||||
id: string;
|
||||
}): NonNullable<LiveTransportQaCliRegistrationOptions["adapterFactory"]> {
|
||||
return {
|
||||
id: params.id,
|
||||
matches: ({ channelId, driver }) => driver === "live" && channelId === params.id,
|
||||
create: params.create,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Qa Lab plugin module implements shared live-transport result shapes.
|
||||
import type { QaEvidenceTiming } from "../../evidence-summary.js";
|
||||
|
||||
type LiveTransportRttMeasurement = {
|
||||
finalMatchedReplyRttMs: number;
|
||||
requestStartedAt: string;
|
||||
responseObservedAt: string;
|
||||
source: "request-to-observed-message";
|
||||
};
|
||||
|
||||
export type LiveTransportCheckResult = {
|
||||
id: string;
|
||||
title: string;
|
||||
status: "pass" | "fail";
|
||||
details: string;
|
||||
timing?: QaEvidenceTiming;
|
||||
rttMs?: number;
|
||||
requestStartedAt?: string;
|
||||
responseObservedAt?: string;
|
||||
rttMeasurement?: LiveTransportRttMeasurement;
|
||||
sentMessageId?: number;
|
||||
responseMessageId?: number;
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runQaSuiteCommand = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../cli.runtime.js", () => ({ runQaSuiteCommand }));
|
||||
|
||||
import { runLiveTransportQaSuiteCommand } from "./live-transport-suite.runtime.js";
|
||||
|
||||
describe("live transport suite runtime", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("normalizes one live command into the shared suite host", async () => {
|
||||
await runLiveTransportQaSuiteCommand({
|
||||
channelId: "slack",
|
||||
defaultProviderMode: "live-frontier",
|
||||
options: {
|
||||
repoRoot: "/repo",
|
||||
outputDir: ".artifacts/slack",
|
||||
primaryModel: "openai/gpt-5.5",
|
||||
alternateModel: "openai/gpt-5.5-alt",
|
||||
fastMode: true,
|
||||
allowFailures: true,
|
||||
failFast: true,
|
||||
credentialSource: " convex ",
|
||||
credentialRole: " ci ",
|
||||
sutAccountId: "slack-sut",
|
||||
},
|
||||
selectScenarioIds: ({ providerMode, scenarioIds }) => {
|
||||
expect(providerMode).toBe("live-frontier");
|
||||
expect(scenarioIds).toBeUndefined();
|
||||
return ["slack-canary"];
|
||||
},
|
||||
});
|
||||
|
||||
expect(runQaSuiteCommand).toHaveBeenCalledWith({
|
||||
repoRoot: "/repo",
|
||||
outputDir: ".artifacts/slack",
|
||||
providerMode: "live-frontier",
|
||||
primaryModel: "openai/gpt-5.5",
|
||||
alternateModel: "openai/gpt-5.5-alt",
|
||||
fastMode: true,
|
||||
allowFailures: true,
|
||||
failFast: true,
|
||||
channelDriver: "live",
|
||||
channel: "slack",
|
||||
concurrency: 1,
|
||||
scenarioIds: ["slack-canary"],
|
||||
sutAccountId: "slack-sut",
|
||||
credentialSource: "convex",
|
||||
credentialRole: "ci",
|
||||
explicitScenarioSelection: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves explicit scenario selection after resolving defaults", async () => {
|
||||
await runLiveTransportQaSuiteCommand({
|
||||
channelId: "whatsapp",
|
||||
defaultProviderMode: "live-frontier",
|
||||
options: { scenarioIds: ["whatsapp-help-command"] },
|
||||
selectScenarioIds: ({ scenarioIds }) => [...(scenarioIds ?? [])],
|
||||
});
|
||||
|
||||
expect(runQaSuiteCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
explicitScenarioSelection: true,
|
||||
scenarioIds: ["whatsapp-help-command"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects shared credentials for disposable transports", async () => {
|
||||
await expect(
|
||||
runLiveTransportQaSuiteCommand({
|
||||
channelId: "matrix",
|
||||
credentialMode: "env-only",
|
||||
defaultProviderMode: "live-frontier",
|
||||
envCredentialReason: "its homeserver is disposable and local.",
|
||||
laneLabel: "Matrix",
|
||||
options: { credentialSource: "convex" },
|
||||
selectScenarioIds: () => ["channel-chat-baseline"],
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"QA Lab Matrix supports only --credential-source env because its homeserver is disposable and local.",
|
||||
);
|
||||
await expect(
|
||||
runLiveTransportQaSuiteCommand({
|
||||
channelId: "matrix",
|
||||
credentialMode: "env-only",
|
||||
defaultProviderMode: "live-frontier",
|
||||
laneLabel: "Matrix",
|
||||
options: { credentialRole: "ci" },
|
||||
selectScenarioIds: () => ["channel-chat-baseline"],
|
||||
}),
|
||||
).rejects.toThrow("QA Lab Matrix does not use credential roles.");
|
||||
expect(runQaSuiteCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects unknown provider modes before suite dispatch", async () => {
|
||||
await expect(
|
||||
runLiveTransportQaSuiteCommand({
|
||||
channelId: "discord",
|
||||
defaultProviderMode: "live-frontier",
|
||||
options: { providerMode: "unknown" },
|
||||
selectScenarioIds: () => ["discord-canary"],
|
||||
}),
|
||||
).rejects.toThrow("unknown QA provider mode: unknown");
|
||||
expect(runQaSuiteCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { LiveTransportQaCommandOptions } from "openclaw/plugin-sdk/qa-runtime";
|
||||
import { runQaSuiteCommand } from "../../cli.runtime.js";
|
||||
import type { QaProviderMode } from "../../providers/index.js";
|
||||
import { normalizeQaProviderMode } from "../../run-config.js";
|
||||
|
||||
type LiveTransportScenarioSelection = (params: {
|
||||
profile?: string;
|
||||
providerMode: QaProviderMode;
|
||||
scenarioIds?: readonly string[];
|
||||
}) => string[];
|
||||
|
||||
export async function runLiveTransportQaSuiteCommand(params: {
|
||||
channelId: string;
|
||||
credentialMode?: "env-only" | "shared-lease";
|
||||
defaultProviderMode: QaProviderMode;
|
||||
envCredentialReason?: string;
|
||||
laneLabel?: string;
|
||||
options: LiveTransportQaCommandOptions;
|
||||
selectScenarioIds: LiveTransportScenarioSelection;
|
||||
}) {
|
||||
const options = params.options;
|
||||
if (params.credentialMode === "env-only") {
|
||||
const laneLabel = params.laneLabel ?? params.channelId;
|
||||
const credentialSource = options.credentialSource?.trim().toLowerCase();
|
||||
if (credentialSource && credentialSource !== "env") {
|
||||
throw new Error(
|
||||
`QA Lab ${laneLabel} supports only --credential-source env${params.envCredentialReason ? ` because ${params.envCredentialReason}` : "."}`,
|
||||
);
|
||||
}
|
||||
if (options.credentialRole?.trim()) {
|
||||
throw new Error(`QA Lab ${laneLabel} does not use credential roles.`);
|
||||
}
|
||||
}
|
||||
|
||||
const providerMode =
|
||||
options.providerMode === undefined
|
||||
? params.defaultProviderMode
|
||||
: normalizeQaProviderMode(options.providerMode);
|
||||
return runQaSuiteCommand({
|
||||
repoRoot: options.repoRoot,
|
||||
outputDir: options.outputDir,
|
||||
providerMode,
|
||||
primaryModel: options.primaryModel,
|
||||
alternateModel: options.alternateModel,
|
||||
fastMode: options.fastMode,
|
||||
allowFailures: options.allowFailures,
|
||||
failFast: options.failFast,
|
||||
channelDriver: "live",
|
||||
channel: params.channelId,
|
||||
concurrency: 1,
|
||||
scenarioIds: params.selectScenarioIds({
|
||||
profile: options.profile,
|
||||
providerMode,
|
||||
scenarioIds: options.scenarioIds,
|
||||
}),
|
||||
sutAccountId: options.sutAccountId,
|
||||
...(params.credentialMode === "env-only"
|
||||
? {}
|
||||
: {
|
||||
credentialSource: options.credentialSource?.trim(),
|
||||
credentialRole: options.credentialRole?.trim(),
|
||||
}),
|
||||
explicitScenarioSelection: Boolean(options.scenarioIds?.length),
|
||||
});
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// QA Lab Slack tests cover CLI delegation into the shared suite host.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runQaSuiteCommand = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../cli.runtime.js", () => ({ runQaSuiteCommand }));
|
||||
vi.mock("../shared/live-transport-cli.runtime.js", () => ({
|
||||
resolveLiveTransportQaRunOptions: (opts: Record<string, unknown>) => ({
|
||||
...opts,
|
||||
repoRoot: "/resolved-repo",
|
||||
outputDir: "/resolved-repo/.artifacts/resolved",
|
||||
providerMode: opts.providerMode ?? "mock-openai",
|
||||
}),
|
||||
}));
|
||||
|
||||
import { listQaScenariosForExecutionProfile } from "../../scenario-catalog.js";
|
||||
import { runQaSlackCommand } from "./cli.runtime.js";
|
||||
|
||||
describe("QA Lab Slack CLI runtime", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("delegates the default profile into the Slack adapter host", async () => {
|
||||
await runQaSlackCommand({
|
||||
repoRoot: "/repo",
|
||||
outputDir: ".artifacts/slack",
|
||||
providerMode: "mock-openai",
|
||||
primaryModel: "mock-openai/gpt-5.5",
|
||||
alternateModel: "mock-openai/gpt-5.5-alt",
|
||||
fastMode: true,
|
||||
allowFailures: true,
|
||||
failFast: true,
|
||||
credentialSource: "convex",
|
||||
credentialRole: "ci",
|
||||
sutAccountId: "slack-sut",
|
||||
});
|
||||
|
||||
expect(runQaSuiteCommand).toHaveBeenCalledWith({
|
||||
repoRoot: "/repo",
|
||||
outputDir: ".artifacts/slack",
|
||||
providerMode: "mock-openai",
|
||||
primaryModel: "mock-openai/gpt-5.5",
|
||||
alternateModel: "mock-openai/gpt-5.5-alt",
|
||||
fastMode: true,
|
||||
allowFailures: true,
|
||||
failFast: true,
|
||||
channelDriver: "live",
|
||||
channel: "slack",
|
||||
concurrency: 1,
|
||||
scenarioIds: listQaScenariosForExecutionProfile("slack:default").map(
|
||||
(scenario) => scenario.id,
|
||||
),
|
||||
sutAccountId: "slack-sut",
|
||||
credentialSource: "convex",
|
||||
credentialRole: "ci",
|
||||
explicitScenarioSelection: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("lets explicit scenarios override profile selection", async () => {
|
||||
await runQaSlackCommand({ scenarioIds: ["slack-codex-approval-exec-native"] });
|
||||
|
||||
expect(runQaSuiteCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
explicitScenarioSelection: true,
|
||||
scenarioIds: ["slack-codex-approval-exec-native"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { runQaSuiteCommand } from "../../cli.runtime.js";
|
||||
import type { LiveTransportQaCommandOptions } from "../shared/live-transport-cli.js";
|
||||
import { resolveLiveTransportQaRunOptions } from "../shared/live-transport-cli.runtime.js";
|
||||
import { resolveSlackQaScenarioIds } from "./profiles.js";
|
||||
|
||||
export async function runQaSlackCommand(opts: LiveTransportQaCommandOptions) {
|
||||
const runOptions = resolveLiveTransportQaRunOptions(opts);
|
||||
return await runQaSuiteCommand({
|
||||
repoRoot: opts.repoRoot,
|
||||
outputDir: opts.outputDir,
|
||||
providerMode: runOptions.providerMode,
|
||||
primaryModel: runOptions.primaryModel,
|
||||
alternateModel: runOptions.alternateModel,
|
||||
fastMode: runOptions.fastMode,
|
||||
allowFailures: runOptions.allowFailures,
|
||||
failFast: runOptions.failFast,
|
||||
channelDriver: "live",
|
||||
channel: "slack",
|
||||
concurrency: 1,
|
||||
scenarioIds: resolveSlackQaScenarioIds(runOptions.scenarioIds),
|
||||
sutAccountId: runOptions.sutAccountId,
|
||||
credentialSource: runOptions.credentialSource,
|
||||
credentialRole: runOptions.credentialRole,
|
||||
explicitScenarioSelection: Boolean(runOptions.scenarioIds?.length),
|
||||
});
|
||||
}
|
||||
@@ -1,37 +1,37 @@
|
||||
// Qa Lab plugin module implements cli behavior.
|
||||
import {
|
||||
createLiveTransportQaAdapterFactory,
|
||||
createLazyCliRuntimeLoader,
|
||||
createLiveTransportQaCliRegistration,
|
||||
loadLiveTransportQaSuiteRuntime,
|
||||
type LiveTransportQaCliRegistration,
|
||||
type LiveTransportQaCommandOptions,
|
||||
} from "../shared/live-transport-cli.js";
|
||||
import { resolveSlackQaScenarioIds } from "./scenario-selection.js";
|
||||
|
||||
type SlackQaAdapterRuntime = typeof import("./adapter.runtime.js");
|
||||
type SlackQaCliRuntime = typeof import("./cli.runtime.js");
|
||||
|
||||
const loadSlackQaAdapterRuntime = createLazyCliRuntimeLoader<SlackQaAdapterRuntime>(
|
||||
const loadSlackQaAdapterRuntime = createLazyCliRuntimeLoader<typeof import("./adapter.runtime.js")>(
|
||||
() => import("./adapter.runtime.js"),
|
||||
);
|
||||
const loadSlackQaCliRuntime = createLazyCliRuntimeLoader<SlackQaCliRuntime>(
|
||||
() => import("./cli.runtime.js"),
|
||||
);
|
||||
|
||||
async function runQaSlack(opts: LiveTransportQaCommandOptions) {
|
||||
await (await loadSlackQaCliRuntime()).runQaSlackCommand(opts);
|
||||
const runtime = await loadLiveTransportQaSuiteRuntime();
|
||||
await runtime.runLiveTransportQaSuiteCommand({
|
||||
channelId: "slack",
|
||||
defaultProviderMode: "live-frontier",
|
||||
options: opts,
|
||||
selectScenarioIds: resolveSlackQaScenarioIds,
|
||||
});
|
||||
}
|
||||
|
||||
const slackQaAdapterFactory: NonNullable<LiveTransportQaCliRegistration["adapterFactory"]> = {
|
||||
id: "slack",
|
||||
matches: ({ channelId, driver }) => driver === "live" && channelId === "slack",
|
||||
async create(context) {
|
||||
return await (await loadSlackQaAdapterRuntime()).createSlackQaTransportAdapter(context);
|
||||
},
|
||||
};
|
||||
|
||||
export const slackQaCliRegistration: LiveTransportQaCliRegistration =
|
||||
createLiveTransportQaCliRegistration({
|
||||
commandName: "slack",
|
||||
adapterFactory: slackQaAdapterFactory,
|
||||
adapterFactory: createLiveTransportQaAdapterFactory({
|
||||
id: "slack",
|
||||
async create(context) {
|
||||
return (await loadSlackQaAdapterRuntime()).createSlackQaTransportAdapter(context);
|
||||
},
|
||||
}),
|
||||
credentialOptions: {
|
||||
sourceDescription: "Credential source for Slack QA: env or convex (default: env)",
|
||||
roleDescription:
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readQaScenarioPack } from "../../scenario-catalog.js";
|
||||
import * as scenarioRuntime from "./scenario-runtime.js";
|
||||
|
||||
describe("legacy Slack scenario migration", () => {
|
||||
it("keeps every retired runner id as a Slack module scenario", () => {
|
||||
const scenarios = readQaScenarioPack().scenarios.filter(
|
||||
(scenario) =>
|
||||
scenario.execution.kind === "flow" &&
|
||||
scenario.execution.channel === "slack" &&
|
||||
JSON.stringify(scenario.execution.flow).includes(
|
||||
"./live-transports/slack/scenario-runtime.js",
|
||||
),
|
||||
);
|
||||
expect(scenarios).toHaveLength(17);
|
||||
for (const scenario of scenarios) {
|
||||
expect(scenario.execution).toMatchObject({ kind: "flow", channel: "slack" });
|
||||
expect(JSON.stringify(scenario.execution.flow)).toContain(
|
||||
"./live-transports/slack/scenario-runtime.js",
|
||||
);
|
||||
const flowText = JSON.stringify(scenario.execution.flow);
|
||||
const callName = flowText.match(/scenarioModule\.([A-Za-z0-9]+)/u)?.[1];
|
||||
expect(typeof scenarioRuntime[callName as keyof typeof scenarioRuntime], scenario.id).toBe(
|
||||
"function",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { listQaScenariosForExecutionProfile } from "../../scenario-catalog.js";
|
||||
|
||||
export function resolveSlackQaScenarioIds(scenarioIds?: readonly string[]) {
|
||||
export function resolveSlackQaScenarioIds({ scenarioIds }: { scenarioIds?: readonly string[] }) {
|
||||
return scenarioIds?.length
|
||||
? [...scenarioIds]
|
||||
: listQaScenariosForExecutionProfile("slack:default").map((scenario) => scenario.id);
|
||||
@@ -1,6 +1,6 @@
|
||||
// Qa Lab tests cover slack live plugin behavior.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveSlackQaScenarioIds } from "./profiles.js";
|
||||
import { resolveSlackQaScenarioIds } from "./scenario-selection.js";
|
||||
import { resolveApprovalDecision } from "./slack-live.approvals.js";
|
||||
import {
|
||||
quiesceCodexApprovalAgentRun,
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
} from "./slack-live.scenarios.js";
|
||||
|
||||
function findScenario(ids?: string[]) {
|
||||
const requestedIds = new Set(ids?.length ? ids : resolveSlackQaScenarioIds());
|
||||
const requestedIds = new Set(ids?.length ? ids : resolveSlackQaScenarioIds({}));
|
||||
return listSlackQaScenarioCatalog()
|
||||
.filter(({ id }) => requestedIds.has(id))
|
||||
.map(({ id }) => getSlackQaScenarioDefinition(id));
|
||||
|
||||
@@ -172,9 +172,10 @@ describe("Telegram QA transport adapter", () => {
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(pollResolvers).toHaveLength(3));
|
||||
mocks.heartbeatStop.mockRejectedValueOnce(new Error("heartbeat stop failed"));
|
||||
const cleanup = adapter.cleanup?.();
|
||||
pollResolvers[2]?.([]);
|
||||
await cleanup;
|
||||
await expect(cleanup).rejects.toThrow("heartbeat stop failed");
|
||||
expect(mocks.heartbeatStop).toHaveBeenCalledOnce();
|
||||
expect(mocks.leaseRelease).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
@@ -45,6 +45,14 @@ export async function createTelegramQaTransportAdapter(
|
||||
throw error;
|
||||
}
|
||||
const heartbeat = startQaCredentialLeaseHeartbeat(credentialLease);
|
||||
const releaseCredentialLease = async () => {
|
||||
// Lease release must still run when heartbeat shutdown reports an error.
|
||||
try {
|
||||
await heartbeat.stop();
|
||||
} finally {
|
||||
await credentialLease.release();
|
||||
}
|
||||
};
|
||||
const runtimeEnv = credentialLease.payload;
|
||||
let driverIdentity: TelegramBotIdentity;
|
||||
let sutIdentity: TelegramBotIdentity;
|
||||
@@ -68,10 +76,10 @@ export async function createTelegramQaTransportAdapter(
|
||||
flushTelegramUpdates(runtimeEnv.sutToken),
|
||||
]);
|
||||
} catch (error) {
|
||||
await heartbeat.stop();
|
||||
await credentialLease.release();
|
||||
await releaseCredentialLease();
|
||||
throw error;
|
||||
}
|
||||
const accountId = options.sutAccountId?.trim() || "sut";
|
||||
let stopped = false;
|
||||
let pollingError: Error | undefined;
|
||||
let logicalConversationId = runtimeEnv.groupId;
|
||||
@@ -112,7 +120,7 @@ export async function createTelegramQaTransportAdapter(
|
||||
if (update.edited_message) {
|
||||
if (existingMessageId) {
|
||||
await context.messages.editMessage({
|
||||
accountId: options.sutAccountId?.trim() || "sut",
|
||||
accountId,
|
||||
messageId: existingMessageId,
|
||||
text: message.text,
|
||||
timestamp: message.timestamp,
|
||||
@@ -121,7 +129,7 @@ export async function createTelegramQaTransportAdapter(
|
||||
continue;
|
||||
}
|
||||
const outbound = await context.messages.addOutboundMessage({
|
||||
accountId: options.sutAccountId?.trim() || "sut",
|
||||
accountId,
|
||||
to: `${logicalConversationKind}:${logicalConversationId}`,
|
||||
senderId: String(message.senderId),
|
||||
senderName: message.senderUsername,
|
||||
@@ -141,8 +149,6 @@ export async function createTelegramQaTransportAdapter(
|
||||
pollingError = error instanceof Error ? error : new Error(String(error));
|
||||
}
|
||||
});
|
||||
const accountId = options.sutAccountId?.trim() || "sut";
|
||||
|
||||
return {
|
||||
id: "telegram",
|
||||
label: "Telegram live",
|
||||
@@ -238,8 +244,7 @@ export async function createTelegramQaTransportAdapter(
|
||||
quarantineErrors.length > 0 ? { cause: new AggregateError(quarantineErrors) } : undefined,
|
||||
);
|
||||
}
|
||||
await heartbeat.stop();
|
||||
await credentialLease.release();
|
||||
await releaseCredentialLease();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { LiveTransportQaCommandOptions } from "openclaw/plugin-sdk/qa-runtime";
|
||||
import type { QaGatewayChildCommand } from "../../gateway-child.js";
|
||||
import { runQaFlowSuiteFromRuntime } from "../../suite-launch.runtime.js";
|
||||
import type { QaSuiteRoundTripProbe } from "../../suite-round-trip.js";
|
||||
import { readQaSuiteFailedScenarioCountFromFile } from "../../suite-summary.js";
|
||||
// Qa Lab plugin module implements cli behavior.
|
||||
import { printLiveTransportQaArtifacts } from "../shared/live-artifacts.js";
|
||||
import type { LiveTransportQaCommandOptions } from "../shared/live-transport-cli.js";
|
||||
import { resolveLiveTransportQaRunOptions } from "../shared/live-transport-cli.runtime.js";
|
||||
import { createTelegramQaTransportAdapter } from "./adapter.runtime.js";
|
||||
import { listTelegramQaScenarios, resolveTelegramQaScenarioIds } from "./profiles.js";
|
||||
import { resolveTelegramQaRunOptions } from "./run-options.runtime.js";
|
||||
|
||||
const TELEGRAM_QA_SUT_OPENCLAW_COMMAND_ENV = "OPENCLAW_QA_TELEGRAM_SUT_OPENCLAW_COMMAND";
|
||||
const TELEGRAM_QA_SUT_UID_ENV = "OPENCLAW_QA_TELEGRAM_SUT_UID";
|
||||
@@ -136,12 +136,13 @@ async function resolveTelegramQaSutOpenClawCommand(
|
||||
}
|
||||
|
||||
type TelegramQaSuiteOptions = LiveTransportQaCommandOptions & {
|
||||
resolvedScenarioIds?: readonly string[];
|
||||
roundTripProbe?: QaSuiteRoundTripProbe;
|
||||
sutOpenClawCommand?: QaGatewayChildCommand;
|
||||
};
|
||||
|
||||
export async function runQaTelegramSuite(opts: TelegramQaSuiteOptions) {
|
||||
const runOptions = resolveLiveTransportQaRunOptions(opts);
|
||||
const runOptions = resolveTelegramQaRunOptions(opts);
|
||||
if (runOptions.listScenarios) {
|
||||
for (const scenario of listTelegramQaScenarios(runOptions.providerMode)) {
|
||||
const defaultLabel = scenario.defaultEnabled ? "default" : "optional";
|
||||
@@ -153,11 +154,13 @@ export async function runQaTelegramSuite(opts: TelegramQaSuiteOptions) {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const scenarioIds = resolveTelegramQaScenarioIds({
|
||||
profile: opts.profile,
|
||||
providerMode: runOptions.providerMode,
|
||||
scenarioIds: runOptions.scenarioIds,
|
||||
});
|
||||
const scenarioIds = opts.resolvedScenarioIds
|
||||
? [...opts.resolvedScenarioIds]
|
||||
: resolveTelegramQaScenarioIds({
|
||||
profile: opts.profile,
|
||||
providerMode: runOptions.providerMode,
|
||||
scenarioIds: runOptions.scenarioIds,
|
||||
});
|
||||
const result = await runQaFlowSuiteFromRuntime({
|
||||
adapterFactories: [
|
||||
{
|
||||
@@ -200,11 +203,11 @@ export async function runQaTelegramSuite(opts: TelegramQaSuiteOptions) {
|
||||
}
|
||||
|
||||
export async function runQaTelegramCommand(opts: LiveTransportQaCommandOptions) {
|
||||
const runOptions = resolveLiveTransportQaRunOptions(opts);
|
||||
const runOptions = resolveTelegramQaRunOptions(opts);
|
||||
if (runOptions.listScenarios) {
|
||||
return await runQaTelegramSuite(opts);
|
||||
}
|
||||
resolveTelegramQaScenarioIds({
|
||||
const resolvedScenarioIds = resolveTelegramQaScenarioIds({
|
||||
profile: opts.profile,
|
||||
providerMode: runOptions.providerMode,
|
||||
scenarioIds: runOptions.scenarioIds,
|
||||
@@ -212,6 +215,7 @@ export async function runQaTelegramCommand(opts: LiveTransportQaCommandOptions)
|
||||
const sutOpenClawCommand = await resolveTelegramQaSutOpenClawCommand(runOptions.repoRoot);
|
||||
return await runQaTelegramSuite({
|
||||
...opts,
|
||||
resolvedScenarioIds,
|
||||
...(sutOpenClawCommand ? { sutOpenClawCommand } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,37 +1,28 @@
|
||||
// Qa Lab plugin module implements cli behavior.
|
||||
import {
|
||||
createLiveTransportQaAdapterFactory,
|
||||
createLazyCliRuntimeLoader,
|
||||
createLiveTransportQaCliRegistration,
|
||||
type LiveTransportQaCliRegistration,
|
||||
type LiveTransportQaCommandOptions,
|
||||
} from "../shared/live-transport-cli.js";
|
||||
|
||||
type TelegramQaAdapterRuntime = typeof import("./adapter.runtime.js");
|
||||
type TelegramQaCliRuntime = typeof import("./cli.runtime.js");
|
||||
|
||||
const loadTelegramQaAdapterRuntime = createLazyCliRuntimeLoader<TelegramQaAdapterRuntime>(
|
||||
() => import("./adapter.runtime.js"),
|
||||
);
|
||||
const loadTelegramQaCliRuntime = createLazyCliRuntimeLoader<TelegramQaCliRuntime>(
|
||||
const loadTelegramQaAdapterRuntime = createLazyCliRuntimeLoader<
|
||||
typeof import("./adapter.runtime.js")
|
||||
>(() => import("./adapter.runtime.js"));
|
||||
const loadTelegramQaCliRuntime = createLazyCliRuntimeLoader<typeof import("./cli.runtime.js")>(
|
||||
() => import("./cli.runtime.js"),
|
||||
);
|
||||
|
||||
async function runQaTelegram(opts: LiveTransportQaCommandOptions) {
|
||||
await (await loadTelegramQaCliRuntime()).runQaTelegramCommand(opts);
|
||||
}
|
||||
|
||||
const telegramQaAdapterFactory: NonNullable<LiveTransportQaCliRegistration["adapterFactory"]> = {
|
||||
id: "telegram",
|
||||
matches: ({ channelId, driver }) => driver === "live" && channelId === "telegram",
|
||||
async create(context) {
|
||||
return await (await loadTelegramQaAdapterRuntime()).createTelegramQaTransportAdapter(context);
|
||||
},
|
||||
};
|
||||
|
||||
export const telegramQaCliRegistration: LiveTransportQaCliRegistration =
|
||||
createLiveTransportQaCliRegistration({
|
||||
commandName: "telegram",
|
||||
adapterFactory: telegramQaAdapterFactory,
|
||||
adapterFactory: createLiveTransportQaAdapterFactory({
|
||||
id: "telegram",
|
||||
async create(context) {
|
||||
return (await loadTelegramQaAdapterRuntime()).createTelegramQaTransportAdapter(context);
|
||||
},
|
||||
}),
|
||||
credentialOptions: {
|
||||
sourceDescription: "Credential source for Telegram QA: env or convex (default: env)",
|
||||
roleDescription:
|
||||
@@ -41,7 +32,9 @@ export const telegramQaCliRegistration: LiveTransportQaCliRegistration =
|
||||
listScenariosHelp: "Print available Telegram scenario ids and exit",
|
||||
outputDirHelp: "Telegram QA artifact directory",
|
||||
profileHelp: "QA Lab Telegram profile: release or all (default: release)",
|
||||
run: runQaTelegram,
|
||||
async run(opts: LiveTransportQaCommandOptions) {
|
||||
await (await loadTelegramQaCliRuntime()).runQaTelegramCommand(opts);
|
||||
},
|
||||
scenarioHelp: "Run only the named Telegram QA scenario (repeatable)",
|
||||
sutAccountHelp: "Temporary Telegram account id inside the QA gateway config",
|
||||
});
|
||||
|
||||
@@ -28,16 +28,16 @@ describe("Telegram QA profiles", () => {
|
||||
).toEqual(["thread-follow-up"]);
|
||||
});
|
||||
|
||||
it("rejects unknown profiles and scenarios before gateway startup", () => {
|
||||
it("rejects unknown profiles and leaves explicit scenario validation to the suite catalog", () => {
|
||||
expect(() =>
|
||||
resolveTelegramQaScenarioIds({ providerMode: "live-frontier", profile: "transport" }),
|
||||
).toThrow('Unknown QA Lab Telegram profile "transport"');
|
||||
expect(() =>
|
||||
expect(
|
||||
resolveTelegramQaScenarioIds({
|
||||
providerMode: "live-frontier",
|
||||
scenarioIds: ["telegram-missing"],
|
||||
scenarioIds: ["channel-chat-baseline"],
|
||||
}),
|
||||
).toThrow("unknown Telegram QA scenario id(s): telegram-missing");
|
||||
).toEqual(["channel-chat-baseline"]);
|
||||
});
|
||||
|
||||
it("lists the YAML catalog with provider-specific release defaults", () => {
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
// Qa Lab tests cover live transport cli plugin behavior.
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveLiveTransportQaRunOptions } from "./live-transport-cli.runtime.js";
|
||||
import { resolveTelegramQaRunOptions } from "./run-options.runtime.js";
|
||||
|
||||
describe("resolveLiveTransportQaRunOptions", () => {
|
||||
describe("resolveTelegramQaRunOptions", () => {
|
||||
it("drops blank model refs so live transports can use provider defaults", () => {
|
||||
const options = resolveLiveTransportQaRunOptions({
|
||||
const options = resolveTelegramQaRunOptions({
|
||||
repoRoot: "/tmp/openclaw-repo",
|
||||
providerMode: "live-frontier",
|
||||
primaryModel: " ",
|
||||
+6
-6
@@ -1,17 +1,17 @@
|
||||
// Qa Lab plugin module implements live transport cli behavior.
|
||||
// Qa Lab plugin module normalizes Telegram live-run options.
|
||||
import path from "node:path";
|
||||
import type { LiveTransportQaCommandOptions } from "openclaw/plugin-sdk/qa-runtime";
|
||||
import { resolveRepoRelativeOutputDir } from "../../cli-paths.js";
|
||||
import { DEFAULT_QA_LIVE_PROVIDER_MODE } from "../../providers/index.js";
|
||||
import type { QaProviderMode } from "../../run-config.js";
|
||||
import { normalizeQaProviderMode } from "../../run-config.js";
|
||||
import type { LiveTransportQaCommandOptions } from "./live-transport-cli.js";
|
||||
|
||||
function normalizeLiveTransportModelRef(input: string | undefined) {
|
||||
function normalizeTelegramModelRef(input: string | undefined) {
|
||||
const model = input?.trim();
|
||||
return model && model.length > 0 ? model : undefined;
|
||||
}
|
||||
|
||||
export function resolveLiveTransportQaRunOptions(
|
||||
export function resolveTelegramQaRunOptions(
|
||||
opts: LiveTransportQaCommandOptions,
|
||||
): LiveTransportQaCommandOptions & {
|
||||
repoRoot: string;
|
||||
@@ -27,8 +27,8 @@ export function resolveLiveTransportQaRunOptions(
|
||||
opts.providerMode === undefined
|
||||
? DEFAULT_QA_LIVE_PROVIDER_MODE
|
||||
: normalizeQaProviderMode(opts.providerMode),
|
||||
primaryModel: normalizeLiveTransportModelRef(opts.primaryModel),
|
||||
alternateModel: normalizeLiveTransportModelRef(opts.alternateModel),
|
||||
primaryModel: normalizeTelegramModelRef(opts.primaryModel),
|
||||
alternateModel: normalizeTelegramModelRef(opts.alternateModel),
|
||||
fastMode: opts.fastMode,
|
||||
allowFailures: opts.allowFailures,
|
||||
failFast: opts.failFast,
|
||||
@@ -1,69 +0,0 @@
|
||||
// QA Lab WhatsApp tests cover CLI delegation into the shared suite host.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runQaSuiteCommand = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../cli.runtime.js", () => ({ runQaSuiteCommand }));
|
||||
vi.mock("../shared/live-transport-cli.runtime.js", () => ({
|
||||
resolveLiveTransportQaRunOptions: (opts: Record<string, unknown>) => ({
|
||||
...opts,
|
||||
repoRoot: "/resolved-repo",
|
||||
outputDir: "/resolved-repo/.artifacts/resolved",
|
||||
providerMode: opts.providerMode ?? "live-frontier",
|
||||
}),
|
||||
}));
|
||||
|
||||
import { runQaWhatsAppCommand } from "./cli.runtime.js";
|
||||
import { resolveWhatsAppQaScenarioIds } from "./profiles.js";
|
||||
|
||||
describe("QA Lab WhatsApp CLI runtime", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("delegates the live profile into the WhatsApp adapter host", async () => {
|
||||
await runQaWhatsAppCommand({
|
||||
repoRoot: "/repo",
|
||||
outputDir: ".artifacts/whatsapp",
|
||||
providerMode: "live-frontier",
|
||||
primaryModel: "openai/gpt-5.5",
|
||||
alternateModel: "openai/gpt-5.5-alt",
|
||||
fastMode: true,
|
||||
allowFailures: true,
|
||||
failFast: true,
|
||||
credentialSource: "convex",
|
||||
credentialRole: "ci",
|
||||
sutAccountId: "whatsapp-sut",
|
||||
});
|
||||
|
||||
expect(runQaSuiteCommand).toHaveBeenCalledWith({
|
||||
repoRoot: "/repo",
|
||||
outputDir: ".artifacts/whatsapp",
|
||||
providerMode: "live-frontier",
|
||||
primaryModel: "openai/gpt-5.5",
|
||||
alternateModel: "openai/gpt-5.5-alt",
|
||||
fastMode: true,
|
||||
allowFailures: true,
|
||||
failFast: true,
|
||||
channelDriver: "live",
|
||||
channel: "whatsapp",
|
||||
concurrency: 1,
|
||||
scenarioIds: resolveWhatsAppQaScenarioIds({ providerMode: "live-frontier" }),
|
||||
sutAccountId: "whatsapp-sut",
|
||||
credentialSource: "convex",
|
||||
credentialRole: "ci",
|
||||
explicitScenarioSelection: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("lets explicit scenarios override profile selection", async () => {
|
||||
await runQaWhatsAppCommand({ scenarioIds: ["whatsapp-help-command"] });
|
||||
|
||||
expect(runQaSuiteCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
explicitScenarioSelection: true,
|
||||
scenarioIds: ["whatsapp-help-command"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { runQaSuiteCommand } from "../../cli.runtime.js";
|
||||
import type { LiveTransportQaCommandOptions } from "../shared/live-transport-cli.js";
|
||||
import { resolveLiveTransportQaRunOptions } from "../shared/live-transport-cli.runtime.js";
|
||||
import { resolveWhatsAppQaScenarioIds } from "./profiles.js";
|
||||
|
||||
export async function runQaWhatsAppCommand(opts: LiveTransportQaCommandOptions) {
|
||||
const runOptions = resolveLiveTransportQaRunOptions(opts);
|
||||
return await runQaSuiteCommand({
|
||||
repoRoot: opts.repoRoot,
|
||||
outputDir: opts.outputDir,
|
||||
providerMode: runOptions.providerMode,
|
||||
primaryModel: runOptions.primaryModel,
|
||||
alternateModel: runOptions.alternateModel,
|
||||
fastMode: runOptions.fastMode,
|
||||
allowFailures: runOptions.allowFailures,
|
||||
failFast: runOptions.failFast,
|
||||
channelDriver: "live",
|
||||
channel: "whatsapp",
|
||||
concurrency: 1,
|
||||
scenarioIds: resolveWhatsAppQaScenarioIds({
|
||||
providerMode: runOptions.providerMode,
|
||||
scenarioIds: runOptions.scenarioIds,
|
||||
}),
|
||||
sutAccountId: runOptions.sutAccountId,
|
||||
credentialSource: runOptions.credentialSource,
|
||||
credentialRole: runOptions.credentialRole,
|
||||
explicitScenarioSelection: Boolean(runOptions.scenarioIds?.length),
|
||||
});
|
||||
}
|
||||
@@ -1,37 +1,37 @@
|
||||
// Qa Lab plugin module implements cli behavior.
|
||||
import {
|
||||
createLiveTransportQaAdapterFactory,
|
||||
createLazyCliRuntimeLoader,
|
||||
createLiveTransportQaCliRegistration,
|
||||
loadLiveTransportQaSuiteRuntime,
|
||||
type LiveTransportQaCliRegistration,
|
||||
type LiveTransportQaCommandOptions,
|
||||
} from "../shared/live-transport-cli.js";
|
||||
import { resolveWhatsAppQaScenarioIds } from "./scenario-selection.js";
|
||||
|
||||
type WhatsAppQaAdapterRuntime = typeof import("./adapter.runtime.js");
|
||||
type WhatsAppQaCliRuntime = typeof import("./cli.runtime.js");
|
||||
|
||||
const loadWhatsAppQaAdapterRuntime = createLazyCliRuntimeLoader<WhatsAppQaAdapterRuntime>(
|
||||
() => import("./adapter.runtime.js"),
|
||||
);
|
||||
const loadWhatsAppQaCliRuntime = createLazyCliRuntimeLoader<WhatsAppQaCliRuntime>(
|
||||
() => import("./cli.runtime.js"),
|
||||
);
|
||||
const loadWhatsAppQaAdapterRuntime = createLazyCliRuntimeLoader<
|
||||
typeof import("./adapter.runtime.js")
|
||||
>(() => import("./adapter.runtime.js"));
|
||||
|
||||
async function runQaWhatsApp(opts: LiveTransportQaCommandOptions) {
|
||||
await (await loadWhatsAppQaCliRuntime()).runQaWhatsAppCommand(opts);
|
||||
const runtime = await loadLiveTransportQaSuiteRuntime();
|
||||
await runtime.runLiveTransportQaSuiteCommand({
|
||||
channelId: "whatsapp",
|
||||
defaultProviderMode: "live-frontier",
|
||||
options: opts,
|
||||
selectScenarioIds: resolveWhatsAppQaScenarioIds,
|
||||
});
|
||||
}
|
||||
|
||||
const whatsappQaAdapterFactory: NonNullable<LiveTransportQaCliRegistration["adapterFactory"]> = {
|
||||
id: "whatsapp",
|
||||
matches: ({ channelId, driver }) => driver === "live" && channelId === "whatsapp",
|
||||
async create(context) {
|
||||
return await (await loadWhatsAppQaAdapterRuntime()).createWhatsAppQaTransportAdapter(context);
|
||||
},
|
||||
};
|
||||
|
||||
export const whatsappQaCliRegistration: LiveTransportQaCliRegistration =
|
||||
createLiveTransportQaCliRegistration({
|
||||
commandName: "whatsapp",
|
||||
adapterFactory: whatsappQaAdapterFactory,
|
||||
adapterFactory: createLiveTransportQaAdapterFactory({
|
||||
id: "whatsapp",
|
||||
async create(context) {
|
||||
return (await loadWhatsAppQaAdapterRuntime()).createWhatsAppQaTransportAdapter(context);
|
||||
},
|
||||
}),
|
||||
credentialOptions: {
|
||||
sourceDescription: "Credential source for WhatsApp QA: env or convex (default: env)",
|
||||
roleDescription:
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readQaScenarioPack } from "../../scenario-catalog.js";
|
||||
import * as scenarioRuntime from "./scenario-runtime.js";
|
||||
|
||||
describe("legacy WhatsApp scenario migration", () => {
|
||||
it("keeps every retired runner id as a WhatsApp module scenario", () => {
|
||||
const scenarios = readQaScenarioPack().scenarios.filter(
|
||||
(scenario) =>
|
||||
scenario.execution.kind === "flow" &&
|
||||
scenario.execution.channel === "whatsapp" &&
|
||||
JSON.stringify(scenario.execution.flow).includes(
|
||||
"./live-transports/whatsapp/scenario-runtime.js",
|
||||
),
|
||||
);
|
||||
expect(scenarios).toHaveLength(38);
|
||||
for (const scenario of scenarios) {
|
||||
expect(scenario.execution).toMatchObject({ kind: "flow", channel: "whatsapp" });
|
||||
expect(JSON.stringify(scenario.execution.flow)).toContain(
|
||||
"./live-transports/whatsapp/scenario-runtime.js",
|
||||
);
|
||||
const flowText = JSON.stringify(scenario.execution.flow);
|
||||
const callName = flowText.match(/scenarioModule\.([A-Za-z0-9]+)/u)?.[1];
|
||||
expect(typeof scenarioRuntime[callName as keyof typeof scenarioRuntime], scenario.id).toBe(
|
||||
"function",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
} from "@openclaw/whatsapp/api.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { fingerprintQaCredentialId } from "../../qa-credentials-fingerprint.runtime.js";
|
||||
import { resolveWhatsAppQaScenarioIds } from "./profiles.js";
|
||||
import { resolveWhatsAppQaScenarioIds } from "./scenario-selection.js";
|
||||
import { runWhatsAppApprovalScenario } from "./whatsapp-live.approvals.js";
|
||||
import { buildWhatsAppQaConfig, parseWhatsAppQaCredentialPayload } from "./whatsapp-live.config.js";
|
||||
import { resolveWhatsAppQaMessageTargets } from "./whatsapp-live.contracts.js";
|
||||
|
||||
Reference in New Issue
Block a user