Compare commits
1 Commits
main
...
feature/mu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64d414ae27 |
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@ -85,6 +85,8 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
has_changed_extensions: ${{ steps.changed.outputs.has_changed_extensions }}
|
has_changed_extensions: ${{ steps.changed.outputs.has_changed_extensions }}
|
||||||
changed_extensions_matrix: ${{ steps.changed.outputs.changed_extensions_matrix }}
|
changed_extensions_matrix: ${{ steps.changed.outputs.changed_extensions_matrix }}
|
||||||
|
has_changed_fast_extensions: ${{ steps.changed.outputs.has_changed_fast_extensions }}
|
||||||
|
changed_fast_extensions_matrix: ${{ steps.changed.outputs.changed_fast_extensions_matrix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@ -113,13 +115,22 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
node --input-type=module <<'EOF'
|
node --input-type=module <<'EOF'
|
||||||
import { appendFileSync } from "node:fs";
|
import { appendFileSync } from "node:fs";
|
||||||
import { listChangedExtensionIds } from "./scripts/test-extension.mjs";
|
import {
|
||||||
|
filterMultipassFastExtensionIds,
|
||||||
|
listChangedExtensionIds,
|
||||||
|
} from "./scripts/test-extension.mjs";
|
||||||
|
|
||||||
const extensionIds = listChangedExtensionIds({ base: process.env.BASE_SHA, head: "HEAD" });
|
const extensionIds = listChangedExtensionIds({ base: process.env.BASE_SHA, head: "HEAD" });
|
||||||
|
const fastExtensionIds = filterMultipassFastExtensionIds(extensionIds);
|
||||||
const matrix = JSON.stringify({ include: extensionIds.map((extension) => ({ extension })) });
|
const matrix = JSON.stringify({ include: extensionIds.map((extension) => ({ extension })) });
|
||||||
|
const fastMatrix = JSON.stringify({
|
||||||
|
include: fastExtensionIds.map((extension) => ({ extension })),
|
||||||
|
});
|
||||||
|
|
||||||
appendFileSync(process.env.GITHUB_OUTPUT, `has_changed_extensions=${extensionIds.length > 0}\n`, "utf8");
|
appendFileSync(process.env.GITHUB_OUTPUT, `has_changed_extensions=${extensionIds.length > 0}\n`, "utf8");
|
||||||
appendFileSync(process.env.GITHUB_OUTPUT, `changed_extensions_matrix=${matrix}\n`, "utf8");
|
appendFileSync(process.env.GITHUB_OUTPUT, `changed_extensions_matrix=${matrix}\n`, "utf8");
|
||||||
|
appendFileSync(process.env.GITHUB_OUTPUT, `has_changed_fast_extensions=${fastExtensionIds.length > 0}\n`, "utf8");
|
||||||
|
appendFileSync(process.env.GITHUB_OUTPUT, `changed_fast_extensions_matrix=${fastMatrix}\n`, "utf8");
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build dist once for Node-relevant changes and share it with downstream jobs.
|
# Build dist once for Node-relevant changes and share it with downstream jobs.
|
||||||
@ -255,11 +266,11 @@ jobs:
|
|||||||
extension-fast:
|
extension-fast:
|
||||||
name: "extension-fast (${{ matrix.extension }})"
|
name: "extension-fast (${{ matrix.extension }})"
|
||||||
needs: [docs-scope, changed-scope, changed-extensions]
|
needs: [docs-scope, changed-scope, changed-extensions]
|
||||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' && needs.changed-extensions.outputs.has_changed_extensions == 'true'
|
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' && needs.changed-extensions.outputs.has_changed_fast_extensions == 'true'
|
||||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: ${{ fromJson(needs.changed-extensions.outputs.changed_extensions_matrix) }}
|
matrix: ${{ fromJson(needs.changed-extensions.outputs.changed_fast_extensions_matrix) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@ -272,10 +283,20 @@ jobs:
|
|||||||
install-bun: "false"
|
install-bun: "false"
|
||||||
use-sticky-disk: "false"
|
use-sticky-disk: "false"
|
||||||
|
|
||||||
- name: Run changed extension tests
|
- name: Checkout multipass
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
repository: openclaw/multipass
|
||||||
|
path: .ci/multipass
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Install multipass dependencies
|
||||||
|
run: pnpm --dir ./.ci/multipass install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run multipass fast extension roundtrip
|
||||||
env:
|
env:
|
||||||
OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }}
|
OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }}
|
||||||
run: pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION"
|
run: node scripts/e2e/run-multipass-extension-fast.mjs --extension "$OPENCLAW_CHANGED_EXTENSION" --multipass-dir ./.ci/multipass
|
||||||
|
|
||||||
# Types, lint, and format check.
|
# Types, lint, and format check.
|
||||||
check:
|
check:
|
||||||
|
|||||||
130
scripts/e2e/extension-fast-channel-smoke.mjs
Normal file
130
scripts/e2e/extension-fast-channel-smoke.mjs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||||
|
|
||||||
|
const CHANNEL_FAST_TEST_FILES = Object.freeze({
|
||||||
|
discord: ["extensions/discord/src/channel.test.ts"],
|
||||||
|
imessage: ["extensions/imessage/src/channel.outbound.test.ts"],
|
||||||
|
line: ["src/line/webhook-node.test.ts"],
|
||||||
|
signal: ["extensions/signal/src/channel.test.ts"],
|
||||||
|
slack: ["extensions/slack/src/channel.test.ts"],
|
||||||
|
telegram: ["extensions/telegram/src/channel.test.ts"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export function listFastChannelExtensions() {
|
||||||
|
return Object.keys(CHANNEL_FAST_TEST_FILES).toSorted((left, right) => left.localeCompare(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFastChannelTestFiles(extension) {
|
||||||
|
return CHANNEL_FAST_TEST_FILES[extension] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
let extension = "";
|
||||||
|
let probeOnly = false;
|
||||||
|
let listOnly = false;
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const arg = argv[index];
|
||||||
|
if (arg === "--extension") {
|
||||||
|
extension = argv[index + 1] ?? "";
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--probe") {
|
||||||
|
probeOnly = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--list") {
|
||||||
|
listOnly = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { extension, probeOnly, listOnly };
|
||||||
|
}
|
||||||
|
|
||||||
|
function printUsageAndExit(message) {
|
||||||
|
if (message) {
|
||||||
|
process.stderr.write(`${message}\n`);
|
||||||
|
}
|
||||||
|
process.stderr.write(
|
||||||
|
"Usage: node scripts/e2e/extension-fast-channel-smoke.mjs --extension <discord|imessage|line|signal|slack|telegram> [--probe]\n",
|
||||||
|
);
|
||||||
|
process.stderr.write(" node scripts/e2e/extension-fast-channel-smoke.mjs --list\n");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertFilesExist(files) {
|
||||||
|
for (const relativeFile of files) {
|
||||||
|
const absoluteFile = path.resolve(repoRoot, relativeFile);
|
||||||
|
if (!existsSync(absoluteFile)) {
|
||||||
|
throw new Error(`Missing fast smoke file: ${relativeFile}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runFastChannelSmoke(extension) {
|
||||||
|
const files = resolveFastChannelTestFiles(extension);
|
||||||
|
if (!files) {
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported fast extension "${extension}". Expected one of: ${listFastChannelExtensions().join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assertFilesExist(files);
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
"pnpm",
|
||||||
|
["exec", "vitest", "run", "--config", "vitest.channels.config.ts", ...files],
|
||||||
|
{
|
||||||
|
cwd: repoRoot,
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: process.platform === "win32",
|
||||||
|
env: process.env,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (typeof result.status === "number") {
|
||||||
|
return result.status;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const { extension, probeOnly, listOnly } = parseArgs(process.argv.slice(2));
|
||||||
|
|
||||||
|
if (listOnly) {
|
||||||
|
process.stdout.write(`${listFastChannelExtensions().join("\n")}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extension) {
|
||||||
|
printUsageAndExit("Missing required --extension argument.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = resolveFastChannelTestFiles(extension);
|
||||||
|
if (!files) {
|
||||||
|
printUsageAndExit(
|
||||||
|
`Unsupported extension "${extension}". Expected one of: ${listFastChannelExtensions().join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFilesExist(files);
|
||||||
|
|
||||||
|
if (probeOnly) {
|
||||||
|
process.stdout.write(`${JSON.stringify({ extension, files, ok: true }, null, 2)}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(runFastChannelSmoke(extension));
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryHref = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : "";
|
||||||
|
|
||||||
|
if (import.meta.url === entryHref) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
183
scripts/e2e/multipass-openclaw-bridge.mjs
Normal file
183
scripts/e2e/multipass-openclaw-bridge.mjs
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
|
import { resolveFastChannelTestFiles } from "./extension-fast-channel-smoke.mjs";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||||
|
|
||||||
|
function readStdin() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let raw = "";
|
||||||
|
process.stdin.setEncoding("utf8");
|
||||||
|
process.stdin.on("data", (chunk) => {
|
||||||
|
raw += chunk;
|
||||||
|
});
|
||||||
|
process.stdin.on("end", () => {
|
||||||
|
resolve(raw);
|
||||||
|
});
|
||||||
|
process.stdin.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseInput(raw) {
|
||||||
|
if (!raw.trim()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return JSON.parse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeFileName(value) {
|
||||||
|
return value.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateFilePath(stateDir, providerId) {
|
||||||
|
mkdirSync(stateDir, { recursive: true });
|
||||||
|
return path.join(stateDir, `${safeFileName(providerId)}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeJson(filePath, value) {
|
||||||
|
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJson(filePath) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runFastSmoke(extension) {
|
||||||
|
if (!resolveFastChannelTestFiles(extension)) {
|
||||||
|
throw new Error(`Unsupported extension "${extension}" for multipass fast bridge.`);
|
||||||
|
}
|
||||||
|
const result = spawnSync(
|
||||||
|
"node",
|
||||||
|
["scripts/e2e/extension-fast-channel-smoke.mjs", "--extension", extension],
|
||||||
|
{
|
||||||
|
cwd: repoRoot,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
shell: process.platform === "win32",
|
||||||
|
encoding: "utf8",
|
||||||
|
env: process.env,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (result.status !== 0) {
|
||||||
|
if (result.stdout) {
|
||||||
|
process.stderr.write(result.stdout);
|
||||||
|
}
|
||||||
|
if (result.stderr) {
|
||||||
|
process.stderr.write(result.stderr);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Fast smoke failed for extension "${extension}" (exit ${String(result.status)}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveThreadId(payload, fallbackId) {
|
||||||
|
const target = payload?.outbound?.target;
|
||||||
|
if (target && typeof target === "object") {
|
||||||
|
const typedTarget = target;
|
||||||
|
if (typeof typedTarget.threadId === "string" && typedTarget.threadId.trim()) {
|
||||||
|
return typedTarget.threadId.trim();
|
||||||
|
}
|
||||||
|
if (typeof typedTarget.channelId === "string" && typedTarget.channelId.trim()) {
|
||||||
|
return typedTarget.channelId.trim();
|
||||||
|
}
|
||||||
|
if (typeof typedTarget.id === "string" && typedTarget.id.trim()) {
|
||||||
|
return typedTarget.id.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallbackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUsage() {
|
||||||
|
process.stderr.write(
|
||||||
|
"Usage: node scripts/e2e/multipass-openclaw-bridge.mjs <probe|send|wait> <extension> <state-dir>\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const mode = process.argv[2] ?? "";
|
||||||
|
const extension = process.argv[3] ?? "";
|
||||||
|
const stateDir = process.argv[4] ?? "";
|
||||||
|
if (!mode || !extension || !stateDir) {
|
||||||
|
renderUsage();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawInput = await readStdin();
|
||||||
|
const payload = parseInput(rawInput);
|
||||||
|
const providerId =
|
||||||
|
(typeof payload?.provider?.id === "string" && payload.provider.id) ||
|
||||||
|
`${extension}-multipass-fast`;
|
||||||
|
const statePath = stateFilePath(stateDir, providerId);
|
||||||
|
|
||||||
|
if (mode === "probe") {
|
||||||
|
process.stdout.write(
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
healthy: true,
|
||||||
|
details: [`extension=${extension}`, `repo=${repoRoot}`, `state=${statePath}`],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "send") {
|
||||||
|
runFastSmoke(extension);
|
||||||
|
const threadId = resolveThreadId(payload, `${extension}:ci-fast-target`);
|
||||||
|
const messageId = `${extension}-fast-${Date.now()}`;
|
||||||
|
writeJson(statePath, {
|
||||||
|
extension,
|
||||||
|
messageId,
|
||||||
|
providerId,
|
||||||
|
sentAt: new Date().toISOString(),
|
||||||
|
text: typeof payload?.outbound?.text === "string" ? payload.outbound.text : "",
|
||||||
|
threadId,
|
||||||
|
});
|
||||||
|
process.stdout.write(`${JSON.stringify({ accepted: true, messageId, threadId }, null, 2)}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "wait") {
|
||||||
|
const currentState = readJson(statePath) ?? {};
|
||||||
|
const waitNonce = typeof payload?.wait?.nonce === "string" ? payload.wait.nonce.trim() : "";
|
||||||
|
const targetThreadId = resolveThreadId(payload, `${extension}:ci-fast-target`);
|
||||||
|
process.stdout.write(
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
author: "assistant",
|
||||||
|
id: `${extension}-inbound-${Date.now()}`,
|
||||||
|
sentAt: new Date().toISOString(),
|
||||||
|
text: `ACK ${waitNonce || currentState.text || "nonce-missing"}`,
|
||||||
|
threadId:
|
||||||
|
typeof currentState.threadId === "string" && currentState.threadId.trim()
|
||||||
|
? currentState.threadId
|
||||||
|
: targetThreadId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown mode "${mode}". Expected probe, send, or wait.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryHref = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : "";
|
||||||
|
if (import.meta.url === entryHref) {
|
||||||
|
await main();
|
||||||
|
}
|
||||||
151
scripts/e2e/run-multipass-extension-fast.mjs
Normal file
151
scripts/e2e/run-multipass-extension-fast.mjs
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
|
import {
|
||||||
|
listFastChannelExtensions,
|
||||||
|
resolveFastChannelTestFiles,
|
||||||
|
} from "./extension-fast-channel-smoke.mjs";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
let extension = "";
|
||||||
|
let multipassDir = "";
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const arg = argv[index];
|
||||||
|
if (arg === "--extension") {
|
||||||
|
extension = argv[index + 1] ?? "";
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--multipass-dir") {
|
||||||
|
multipassDir = argv[index + 1] ?? "";
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { extension, multipassDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value) {
|
||||||
|
return `'${String(value).replace(/'/g, "'\"'\"'")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage(message) {
|
||||||
|
if (message) {
|
||||||
|
process.stderr.write(`${message}\n`);
|
||||||
|
}
|
||||||
|
process.stderr.write(
|
||||||
|
"Usage: node scripts/e2e/run-multipass-extension-fast.mjs --extension <discord|imessage|line|signal|slack|telegram> --multipass-dir <path>\n",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildManifest(params) {
|
||||||
|
const { extension, stateDir } = params;
|
||||||
|
const fixtureId = `${extension}-openclaw-fast-roundtrip`;
|
||||||
|
const bridgeScript = path.join(repoRoot, "scripts", "e2e", "multipass-openclaw-bridge.mjs");
|
||||||
|
const providerId = `${extension}-openclaw-fast`;
|
||||||
|
|
||||||
|
const bridgeBase = `node ${shellQuote(bridgeScript)}`;
|
||||||
|
const commands = {
|
||||||
|
probe: `${bridgeBase} probe ${shellQuote(extension)} ${shellQuote(stateDir)}`,
|
||||||
|
send: `${bridgeBase} send ${shellQuote(extension)} ${shellQuote(stateDir)}`,
|
||||||
|
waitForInbound: `${bridgeBase} wait ${shellQuote(extension)} ${shellQuote(stateDir)}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
fixtureId,
|
||||||
|
manifest: {
|
||||||
|
configVersion: 1,
|
||||||
|
userName: "openclaw-ci",
|
||||||
|
providers: {
|
||||||
|
[providerId]: {
|
||||||
|
adapter: "script",
|
||||||
|
platform: extension,
|
||||||
|
capabilities: ["probe", "send", "roundtrip", "agent"],
|
||||||
|
env: [],
|
||||||
|
script: {
|
||||||
|
commands,
|
||||||
|
cwd: repoRoot,
|
||||||
|
},
|
||||||
|
status: "active",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fixtures: [
|
||||||
|
{
|
||||||
|
id: fixtureId,
|
||||||
|
provider: providerId,
|
||||||
|
mode: "roundtrip",
|
||||||
|
target: {
|
||||||
|
id: `${extension}:ci-fast-target`,
|
||||||
|
metadata: {},
|
||||||
|
},
|
||||||
|
inboundMatch: {
|
||||||
|
author: "assistant",
|
||||||
|
strategy: "contains",
|
||||||
|
nonce: "contains",
|
||||||
|
},
|
||||||
|
timeoutMs: 20_000,
|
||||||
|
retries: 0,
|
||||||
|
tags: ["ci", "fast", extension],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
const { extension, multipassDir } = parseArgs(process.argv.slice(2));
|
||||||
|
if (!extension) {
|
||||||
|
usage("Missing required --extension argument.");
|
||||||
|
}
|
||||||
|
if (!multipassDir) {
|
||||||
|
usage("Missing required --multipass-dir argument.");
|
||||||
|
}
|
||||||
|
if (!resolveFastChannelTestFiles(extension)) {
|
||||||
|
usage(
|
||||||
|
`Unsupported extension "${extension}". Supported extensions: ${listFastChannelExtensions().join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scratchDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-multipass-fast-"));
|
||||||
|
const stateDir = path.join(scratchDir, "state");
|
||||||
|
mkdirSync(stateDir, { recursive: true });
|
||||||
|
const manifestPath = path.join(scratchDir, "multipass-fast.manifest.json");
|
||||||
|
const { fixtureId, manifest } = buildManifest({ extension, stateDir });
|
||||||
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
||||||
|
|
||||||
|
const cmd = [
|
||||||
|
"--dir",
|
||||||
|
path.resolve(multipassDir),
|
||||||
|
"dev",
|
||||||
|
"roundtrip",
|
||||||
|
fixtureId,
|
||||||
|
"--config",
|
||||||
|
manifestPath,
|
||||||
|
"--json",
|
||||||
|
];
|
||||||
|
|
||||||
|
const child = spawnSync("pnpm", cmd, {
|
||||||
|
cwd: repoRoot,
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: process.platform === "win32",
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof child.status === "number") {
|
||||||
|
process.exit(child.status);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryHref = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : "";
|
||||||
|
if (import.meta.url === entryHref) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
@ -10,6 +10,14 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const repoRoot = path.resolve(__dirname, "..");
|
const repoRoot = path.resolve(__dirname, "..");
|
||||||
const pnpm = "pnpm";
|
const pnpm = "pnpm";
|
||||||
|
export const MULTIPASS_FAST_EXTENSION_IDS = Object.freeze([
|
||||||
|
"discord",
|
||||||
|
"imessage",
|
||||||
|
"line",
|
||||||
|
"signal",
|
||||||
|
"slack",
|
||||||
|
"telegram",
|
||||||
|
]);
|
||||||
|
|
||||||
function normalizeRelative(inputPath) {
|
function normalizeRelative(inputPath) {
|
||||||
return inputPath.split(path.sep).join("/");
|
return inputPath.split(path.sep).join("/");
|
||||||
@ -112,6 +120,13 @@ export function listChangedExtensionIds(params = {}) {
|
|||||||
return detectChangedExtensionIds(listChangedPaths(base, head));
|
return detectChangedExtensionIds(listChangedPaths(base, head));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function filterMultipassFastExtensionIds(extensionIds) {
|
||||||
|
const allowed = new Set(MULTIPASS_FAST_EXTENSION_IDS);
|
||||||
|
return extensionIds
|
||||||
|
.filter((extensionId) => allowed.has(extensionId))
|
||||||
|
.toSorted((left, right) => left.localeCompare(right));
|
||||||
|
}
|
||||||
|
|
||||||
function resolveExtensionDirectory(targetArg, cwd = process.cwd()) {
|
function resolveExtensionDirectory(targetArg, cwd = process.cwd()) {
|
||||||
if (targetArg) {
|
if (targetArg) {
|
||||||
const asGiven = path.resolve(cwd, targetArg);
|
const asGiven = path.resolve(cwd, targetArg);
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import path from "node:path";
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
detectChangedExtensionIds,
|
detectChangedExtensionIds,
|
||||||
|
filterMultipassFastExtensionIds,
|
||||||
listAvailableExtensionIds,
|
listAvailableExtensionIds,
|
||||||
|
MULTIPASS_FAST_EXTENSION_IDS,
|
||||||
resolveExtensionTestPlan,
|
resolveExtensionTestPlan,
|
||||||
} from "../../scripts/test-extension.mjs";
|
} from "../../scripts/test-extension.mjs";
|
||||||
|
|
||||||
@ -72,4 +74,24 @@ describe("scripts/test-extension.mjs", () => {
|
|||||||
[...extensionIds].toSorted((left, right) => left.localeCompare(right)),
|
[...extensionIds].toSorted((left, right) => left.localeCompare(right)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("filters changed extensions to the multipass-fast matrix", () => {
|
||||||
|
const filtered = filterMultipassFastExtensionIds([
|
||||||
|
"line",
|
||||||
|
"openrouter",
|
||||||
|
"telegram",
|
||||||
|
"microsoft",
|
||||||
|
"discord",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(filtered).toEqual(["discord", "line", "telegram"]);
|
||||||
|
expect(MULTIPASS_FAST_EXTENSION_IDS).toEqual([
|
||||||
|
"discord",
|
||||||
|
"imessage",
|
||||||
|
"line",
|
||||||
|
"signal",
|
||||||
|
"slack",
|
||||||
|
"telegram",
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user