




















@@ -18,6 +18,8 @@ import {
1818buildGauntletPrebuildEnv,
1919collectGatewayCpuObservations,
2020collectMetricObservations,
21+collectPluginsWithRequiredEntries,
22+collectRequiredPluginEntries,
2123collectQaBaselineRegressionObservations,
2224detectCommandDiagnosticFailure,
2325discoverBundledPluginManifests,
@@ -160,6 +162,7 @@ describe("plugin gateway gauntlet helpers", () => {
160162name: "alpha",
161163onboardingScopes: ["models"],
162164providers: ["openai"],
165+requiredPlugins: [],
163166runtimeSlashAliases: [{ name: "alpha", kind: "runtime-slash", cliCommand: "plugins" }],
164167skills: [],
165168});
@@ -223,6 +226,49 @@ describe("plugin gateway gauntlet helpers", () => {
223226);
224227});
225228229+it("collects required plugin support outside the selected shard", () => {
230+const entries = [
231+{ id: "alpha", requiredPlugins: ["beta"] },
232+{ id: "beta", requiredPlugins: ["gamma"] },
233+{ id: "gamma" },
234+{ id: "delta" },
235+];
236+const selected = selectPluginEntries(entries, {
237+ids: ["alpha"],
238+shardTotal: 2,
239+shardIndex: 0,
240+});
241+242+expect(collectRequiredPluginEntries(entries, selected).map((entry) => entry.id)).toEqual([
243+"gamma",
244+"beta",
245+]);
246+expect(collectPluginsWithRequiredEntries(entries, selected).map((entry) => entry.id)).toEqual([
247+"gamma",
248+"beta",
249+"alpha",
250+]);
251+});
252+253+it("rejects missing bundled plugin requirements", () => {
254+const entries = [{ id: "alpha", requiredPlugins: ["missing"] }];
255+256+expect(() => collectRequiredPluginEntries(entries, entries)).toThrow(
257+'Bundled plugin "alpha" requires unknown bundled plugin "missing"',
258+);
259+});
260+261+it("rejects bundled plugin requirement cycles", () => {
262+const entries = [
263+{ id: "alpha", requiredPlugins: ["beta"] },
264+{ id: "beta", requiredPlugins: ["alpha"] },
265+];
266+267+expect(() => collectRequiredPluginEntries(entries, [entries[0]])).toThrow(
268+"Bundled plugin dependency cycle detected: alpha -> beta -> alpha",
269+);
270+});
271+226272it("detects required schema fields recursively", () => {
227273expect(
228274schemaHasRequiredFields({
@@ -1038,13 +1084,19 @@ setInterval(() => {}, 1000);
10381084]);
10391085});
104010861041-it("carries bounded build ids into QA run-node chunks", async () => {
1087+it("carries required plugin build ids and enables dependencies in QA chunks", async () => {
10421088const outputDir = path.join(repoRoot, "artifacts");
10431089const qaSummaryJson = JSON.stringify(
10441090minimalQaSuiteSummary({ gatewayCpuCoreRatio: 0, wallMs: 1 }),
10451091);
1046-await writeManifest("alpha", "openclaw.plugin.json", JSON.stringify({ id: "alpha" }));
1092+await writeManifest(
1093+"alpha",
1094+"openclaw.plugin.json",
1095+JSON.stringify({ id: "alpha", requiresPlugins: ["beta"] }),
1096+);
1097+await writeManifest("beta", "openclaw.plugin.json", JSON.stringify({ id: "beta" }));
10471098await fs.writeFile(path.join(repoRoot, "extensions", "alpha", "index.ts"), "export {};\n");
1099+await fs.writeFile(path.join(repoRoot, "extensions", "beta", "index.ts"), "export {};\n");
10481100await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true });
10491101await fs.writeFile(
10501102path.join(repoRoot, "scripts", "run-node.mjs"),
@@ -1055,6 +1107,7 @@ setInterval(() => {}, 1000);
10551107"const outputDir = path.resolve(process.cwd(), process.argv[outputArgIndex + 1]);",
10561108"fs.mkdirSync(outputDir, { recursive: true });",
10571109'fs.writeFileSync(path.join(outputDir, "env.txt"), process.env.OPENCLAW_BUNDLED_PLUGIN_BUILD_IDS ?? "", "utf8");',
1110+'fs.writeFileSync(path.join(outputDir, "args.txt"), process.argv.slice(2).join("\\n"), "utf8");',
10581111`fs.writeFileSync(path.join(outputDir, "qa-suite-summary.json"), ${JSON.stringify(qaSummaryJson)}, "utf8");`,
10591112].join("\n"),
10601113"utf8",
@@ -1085,7 +1138,63 @@ setInterval(() => {}, 1000);
10851138expect(result.status, result.stderr).toBe(0);
10861139await expect(
10871140fs.readFile(path.join(outputDir, "qa-suite", "chunk-00", "env.txt"), "utf8"),
1088-).resolves.toBe("alpha,qa-channel,qa-lab,qa-matrix");
1141+).resolves.toBe("alpha,beta,qa-channel,qa-lab,qa-matrix");
1142+await expect(
1143+fs.readFile(path.join(outputDir, "qa-suite", "chunk-00", "args.txt"), "utf8"),
1144+).resolves.toContain(["--enable-plugin", "beta", "--enable-plugin", "alpha"].join("\n"));
1145+});
1146+1147+it("installs required plugins around a dependent plugin lifecycle probe", async () => {
1148+const outputDir = path.join(repoRoot, "artifacts");
1149+await writeManifest(
1150+"alpha",
1151+"openclaw.plugin.json",
1152+JSON.stringify({ id: "alpha", requiresPlugins: ["beta"] }),
1153+);
1154+await writeManifest("beta", "openclaw.plugin.json", JSON.stringify({ id: "beta" }));
1155+await fs.writeFile(path.join(repoRoot, "extensions", "alpha", "index.ts"), "export {};\n");
1156+await fs.writeFile(path.join(repoRoot, "extensions", "beta", "index.ts"), "export {};\n");
1157+await fs.mkdir(path.join(repoRoot, "dist"), { recursive: true });
1158+await fs.writeFile(
1159+path.join(repoRoot, "dist", "entry.js"),
1160+"if (process.argv[3] === 'inspect') console.log('{}');\n",
1161+"utf8",
1162+);
1163+1164+const result = spawnSync(
1165+process.execPath,
1166+[
1167+path.resolve("scripts/check-plugin-gateway-gauntlet.mjs"),
1168+"--repo-root",
1169+repoRoot,
1170+"--output-dir",
1171+outputDir,
1172+"--skip-prebuild",
1173+"--skip-qa",
1174+"--skip-slash-help",
1175+"--plugin",
1176+"alpha",
1177+],
1178+{
1179+cwd: path.resolve("."),
1180+encoding: "utf8",
1181+},
1182+);
1183+1184+expect(result.status, result.stderr).toBe(0);
1185+const summary = JSON.parse(
1186+await fs.readFile(path.join(outputDir, "plugin-gateway-gauntlet-summary.json"), "utf8"),
1187+);
1188+expect(summary.rows.map((row: { label: string }) => row.label)).toEqual([
1189+"alpha-requires-beta-install",
1190+"alpha-install",
1191+"alpha-inspect",
1192+"alpha-disable",
1193+"alpha-enable",
1194+"alpha-doctor",
1195+"alpha-uninstall",
1196+"alpha-requires-beta-uninstall",
1197+]);
10891198});
1090119910911200it("fails successful QA chunks whose summary reports failed scenarios", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。