test: stabilize release docker e2e harnesses

This commit is contained in:
Tideclaw
2026-06-01 12:08:09 +00:00
parent 8862bfba7c
commit dfa0e9485c
3 changed files with 75 additions and 31 deletions

View File

@@ -108,7 +108,16 @@ function pluginRequiresConfig(pluginDir) {
}
const manifest = readJson(manifestPath);
const required = manifest.configSchema?.required;
return Array.isArray(required) && required.some((value) => typeof value === "string");
if (Array.isArray(required) && required.some((value) => typeof value === "string")) {
return true;
}
const channelEnvVars =
manifest.channelEnvVars && typeof manifest.channelEnvVars === "object"
? Object.values(manifest.channelEnvVars)
: [];
return channelEnvVars.some(
(envVars) => Array.isArray(envVars) && envVars.some((value) => typeof value === "string"),
);
}
async function loadPackagedBundledEntries() {

View File

@@ -352,32 +352,43 @@ export async function connectMcpClient(params: {
export async function maybeApprovePendingBridgePairing(
gateway: GatewayRpcClient,
): Promise<boolean> {
let pairingState:
| {
for (let attempt = 0; attempt < 2; attempt += 1) {
let pairingState:
| {
pending?: Array<{ requestId?: string; role?: string }>;
}
| undefined;
try {
pairingState = await gateway.request<{
pending?: Array<{ requestId?: string; role?: string }>;
}>("device.pair.list", {});
} catch (error) {
const message = formatErrorMessage(error);
if (
message.includes("missing scope: operator.pairing") ||
message.includes("device.pair.list")
) {
return false;
}
| undefined;
try {
pairingState = await gateway.request<{
pending?: Array<{ requestId?: string; role?: string }>;
}>("device.pair.list", {});
} catch (error) {
const message = formatErrorMessage(error);
if (
message.includes("missing scope: operator.pairing") ||
message.includes("device.pair.list")
) {
throw error;
}
if (!pairingState) {
return false;
}
throw error;
const pendingRequest = pairingState.pending?.find((entry) => entry.role === "operator");
if (!pendingRequest?.requestId) {
return false;
}
try {
await gateway.request("device.pair.approve", { requestId: pendingRequest.requestId });
return true;
} catch (error) {
if (!formatErrorMessage(error).includes("unknown requestId")) {
throw error;
}
// The gateway may auto-approve the same bridge request between list and approve.
// Reconnect the MCP client when no replacement request is left to approve.
}
}
if (!pairingState) {
return false;
}
const pendingRequest = pairingState.pending?.find((entry) => entry.role === "operator");
if (!pendingRequest?.requestId) {
return false;
}
await gateway.request("device.pair.approve", { requestId: pendingRequest.requestId });
return true;
}

View File

@@ -198,9 +198,7 @@ describe("bundled plugin install/uninstall probe", () => {
"qa-channel",
),
).not.toThrow();
expect(() =>
runtimeSmoke.assertChannelVisible({}, "qa-channel", "qa-channel"),
).toThrow(
expect(() => runtimeSmoke.assertChannelVisible({}, "qa-channel", "qa-channel")).toThrow(
"Runtime channel status missing manifest channel qa-channel for qa-channel",
);
});
@@ -380,11 +378,15 @@ describe("bundled plugin install/uninstall probe", () => {
);
await expect(
runtimeSmoke.rpcCall("health", {}, {
entrypoint,
env: { OPENCLAW_TEST_RPC_STATE_PATH: statePath },
port: 19001,
}),
runtimeSmoke.rpcCall(
"health",
{},
{
entrypoint,
env: { OPENCLAW_TEST_RPC_STATE_PATH: statePath },
port: 19001,
},
),
).resolves.toEqual({ status: "ok" });
const rpcStateDir = fs.readFileSync(statePath, "utf8");
@@ -515,6 +517,28 @@ describe("bundled plugin install/uninstall probe", () => {
);
});
it("treats channel env vars as runtime smoke config requirements", () => {
const root = makePackageRoot();
writePluginManifest(root, "dist-runtime/extensions/clickclack", {
id: "clickclack",
channelEnvVars: { clickclack: ["CLICKCLACK_BOT_TOKEN"] },
});
writePluginsList(root, [
{
id: "clickclack",
origin: "bundled",
rootDir: path.join(root, "dist-runtime", "extensions", "clickclack"),
},
]);
const result = runProbe(root);
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe(
`clickclack\tclickclack\t1\t${path.join(root, "dist-runtime", "extensions", "clickclack")}`,
);
});
it("does not select source-only bundled plugins for package-backed sweeps", () => {
const root = makePackageRoot();
writePluginManifest(root, "extensions/qa-channel", {