@@ -18,6 +18,8 @@ import {
1818 buildGauntletPrebuildEnv ,
1919 collectGatewayCpuObservations ,
2020 collectMetricObservations ,
21+ collectPluginsWithRequiredEntries ,
22+ collectRequiredPluginEntries ,
2123 collectQaBaselineRegressionObservations ,
2224 detectCommandDiagnosticFailure ,
2325 discoverBundledPluginManifests ,
@@ -160,6 +162,7 @@ describe("plugin gateway gauntlet helpers", () => {
160162 name : "alpha" ,
161163 onboardingScopes : [ "models" ] ,
162164 providers : [ "openai" ] ,
165+ requiredPlugins : [ ] ,
163166 runtimeSlashAliases : [ { name : "alpha" , kind : "runtime-slash" , cliCommand : "plugins" } ] ,
164167 skills : [ ] ,
165168 } ) ;
@@ -223,6 +226,49 @@ describe("plugin gateway gauntlet helpers", () => {
223226 ) ;
224227 } ) ;
225228
229+ 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+
226272 it ( "detects required schema fields recursively" , ( ) => {
227273 expect (
228274 schemaHasRequiredFields ( {
@@ -1038,13 +1084,19 @@ setInterval(() => {}, 1000);
10381084 ] ) ;
10391085 } ) ;
10401086
1041- 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 ( ) => {
10421088 const outputDir = path . join ( repoRoot , "artifacts" ) ;
10431089 const qaSummaryJson = JSON . stringify (
10441090 minimalQaSuiteSummary ( { 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" } ) ) ;
10471098 await fs . writeFile ( path . join ( repoRoot , "extensions" , "alpha" , "index.ts" ) , "export {};\n" ) ;
1099+ await fs . writeFile ( path . join ( repoRoot , "extensions" , "beta" , "index.ts" ) , "export {};\n" ) ;
10481100 await fs . mkdir ( path . join ( repoRoot , "scripts" ) , { recursive : true } ) ;
10491101 await fs . writeFile (
10501102 path . 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);
10851138 expect ( result . status , result . stderr ) . toBe ( 0 ) ;
10861139 await expect (
10871140 fs . 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 } ) ;
10901199
10911200 it ( "fails successful QA chunks whose summary reports failed scenarios" , async ( ) => {
0 commit comments