mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
* fix: recognize verified team admins as channel owners
Resolve channel owner authority from SQLite profile identity links and current login grants, with immediate revocation across deferred work. Keep Gateway and bundled plugins on one native SDK graph and replace the related process-global authority registries with instance-owned capabilities.
* fix: preserve channel owner authority through deferred work
Authorize Discord commands before ACP preparation and retain the original live owner through backend effects and updater handoffs. Normalize direct notice recipients through channel contracts. Complete native test-loader and instance-runtime fixture coverage, including final-effect and revocation regressions.
* fix(plugins): retain host SDK access in captured workers
Link captured plugins to the selected host package for worker isolates, preserving native SDK identity through retained generations and recovery. Align CI fixtures with instance-owned runtimes and join owned asynchronous work in teardown and Telegram buffering tests.
Validation: native worker regression fails before the fix and passes across native/legacy and source/dist hosts; 325 core tests, the full 3121-test Slack suite, targeted channel tests, protocol generation, Android lint, changed checks, and independent review pass.
* fix(runtime): keep snapshot cleanup inside owned directories
Treat captured SDK host-package links as removable leaves, preserving ownership records until snapshot data is gone. Preserve sanitized readiness subprocess failures and exercise Doctor through its complete isolated runtime on clean installations.
* fix(plugins): keep lazy runtime ownership metadata local
Preserve deferred Gateway facets with instance-owned proxy metadata and retain redacted readiness failure diagnostics on the current subprocess result owner.
* fix(auth): retain live owner authority through command effects
Carry the admitted administrator assertion through command dispatch, ACP controls and metadata commits, config and allowlist writes, plugin consent and installation, MCP mutation, and restart preparation. Preserve accepted-operation settlement and condition restart acknowledgement cleanup on its owned revision. Prove allowed administrators, forbidden senders, revocation and reassignment through real handler and persistence boundaries.
* refactor(restart): require owned revisions for sentinel cleanup
Remove the unused unconditional clear facade and storage branch. Keep revision-floor migration, durable failure reporting, and updater consumption proof on the canonical conditional-clear operation.
* test(auth): align owner regressions with fixture lint contracts
* refactor(auth): simplify channel owner and runtime authority
Resolve linked channel administration from the current Team role policy, retaining identity-grant fallback only for roleless installations. Consolidate Gateway generation state into its lifecycle owner, simplify Discord native routing and remove redundant loader and ingress state. Preserve current-authority checks before writes and required cleanup after accepted operations.
* test(auth): compare public generation state values
* refactor(auth): keep authority fixtures and handoff types with their owners
Extract coherent fixture builders and internal updater types to keep large files from growing. Correct the task-identifier test import to its codec owner and remove the unused internal route-policy export. Preserve all runtime behavior, assertions, deadlines and revision-owned sentinel cleanup.
* fix(channels): preserve native conversation scope in ingress authority
* fix(imessage): bind ingress after reply ID mapping
* fix(test): preserve scoped filesystem and channel admission contracts
* test(fleet): share stopped container state fixture
* test(fleet): type stop mock against the container contract
* fix(auth): retain current owner authority through deferred effects
* refactor(auth): keep authority fixtures and helpers with their owners
* fix(ci): remove duplicate database worker test entry
Retain the existing worker.runtime test entry so compact CI planning includes every storage-state test once. The duplicate introduced in d2c8c34af2 made preflight reject all split timing generations for this owner.
Reproduced the exact preflight error before the repair. Hybrid, GitHub, and Blacksmith planning now preserve all 660 unique storage-state files. The 217 planner/config tests, selected changed-file checks, formatting, and diff checks pass.
* refactor(auth): prepare profile authority in SQLite workers
Move channel identity and affected profile writes onto the existing worker owners, with current authority at commit and explicit rollback recovery. Bind native Telegram commands to verified ingress and retain shared-owner administration.
* fix(auth): preserve owner checks and released ingress callers
Forward Telegram authority through configured backend preparation and retain released ingress helper provenance through the existing plugin instance owner. Keep identity result types in the leaf contract and repair worker-aware test routing and fixtures without weakening policy assertions.
* fix: correct ingress names and database test ownership
Keep supported SDK ingress adapters while distinguishing internal policy operations. Assign broker-dependent HTTP suites only to the Gateway fork owner, preserve sorted test discovery, reuse chat registration fixtures, and declare the dynamically loaded Telegram test entry.
* test(cli): use prepared runtime for MCP probe exit
Exercise the real CLI entrypoint instead of compiling source-backed SQLite workers inside the command deadline. Keep the 30-second deadline and the exit, JSON, and named diagnostic checks.
* test(codex): check native worker termination at teardown
Observe native Worker thread IDs after fixture cleanup instead of equating
thread exit with asynchronous resource-destroy notification. Keep the
allocation assertion and existing cleanup; do not wait for idle retirement.
The three-case file passes in 48.495s. Omitting only its harness disk-worker
drain fails immediately on a live thread in 30.715s. A Node 24.19 control
observes threadId -1 while the async destroy notification is still pending.
Managed review is clean through P2. The earlier CI worker identity remains
unproven; this is a test-contract repair, not a production leak claim.
* test: settle identity fixtures and route database cleanup
Wait for actual GitHub metadata entry and settle both identity requests on failure. Run the session-store consumer in the existing database fork owner so native retirement can use the host broker.
* test: settle admin fixtures at their owning boundaries
Await canonical asynchronous MCP OAuth reads and the existing Gateway attachment completion. Keep avatar work independently gated and preserve all permission assertions. Move ingress and callback fixture helpers into the existing support owner to keep the health suite within its line-growth limit.
* test(worker): share the compiled SDK graph in crash fixtures
488 lines
18 KiB
TypeScript
488 lines
18 KiB
TypeScript
// Smoke-tests the built plugin loader singleton and bundled plugin runtime overlay.
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { withDistArtifactOwnership } from "./lib/dist-artifact-ownership.mts";
|
|
import { resolveRepoRoot } from "./lib/repo-root.mjs";
|
|
import { installProcessWarningFilter } from "./process-warning-filter.mts";
|
|
import { prepareBundledPluginRuntime } from "./stage-bundled-plugin-runtime.mts";
|
|
|
|
installProcessWarningFilter();
|
|
|
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
|
async function runBuiltPluginSingletonSmoke(signal: AbortSignal) {
|
|
signal.throwIfAborted();
|
|
const smokeEntryPath = path.join(repoRoot, "dist", "plugins", "build-smoke-entry.js");
|
|
assert.ok(fs.existsSync(smokeEntryPath), `missing build output: ${smokeEntryPath}`);
|
|
|
|
const {
|
|
buildPluginRuntimeLoadOptions,
|
|
clearPluginCommands,
|
|
getPluginCommandSpecs,
|
|
getPluginModuleLoaderStats,
|
|
loadOpenClawPlugins,
|
|
matchPluginCommand,
|
|
resolvePluginRuntimeLoadContext,
|
|
setPluginRuntimeLoadContext,
|
|
withPluginRuntimeGenerationScope,
|
|
} = await import(pathToFileURL(smokeEntryPath).href);
|
|
signal.throwIfAborted();
|
|
|
|
assert.equal(typeof loadOpenClawPlugins, "function", "built loader export missing");
|
|
assert.equal(typeof clearPluginCommands, "function", "clearPluginCommands missing");
|
|
assert.equal(typeof getPluginCommandSpecs, "function", "getPluginCommandSpecs missing");
|
|
assert.equal(typeof getPluginModuleLoaderStats, "function", "plugin loader stats missing");
|
|
assert.equal(typeof matchPluginCommand, "function", "matchPluginCommand missing");
|
|
|
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-build-smoke-"));
|
|
const pluginId = "build-smoke-plugin";
|
|
const distPluginDir = path.join(repoRoot, "dist", "extensions", pluginId);
|
|
const runtimePluginDir = path.join(repoRoot, "dist-runtime", "extensions", pluginId);
|
|
|
|
function cleanup() {
|
|
clearPluginCommands();
|
|
fs.rmSync(distPluginDir, { recursive: true, force: true });
|
|
fs.rmSync(runtimePluginDir, { recursive: true, force: true });
|
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
}
|
|
|
|
process.on("exit", cleanup);
|
|
|
|
try {
|
|
fs.mkdirSync(distPluginDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(distPluginDir, "package.json"),
|
|
JSON.stringify(
|
|
{
|
|
name: "@openclaw/build-smoke-plugin",
|
|
type: "module",
|
|
openclaw: {
|
|
extensions: ["./index.js"],
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
"utf8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(distPluginDir, "openclaw.plugin.json"),
|
|
JSON.stringify(
|
|
{
|
|
id: pluginId,
|
|
configSchema: {
|
|
type: "object",
|
|
additionalProperties: false,
|
|
properties: {},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
"utf8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(distPluginDir, "index.js"),
|
|
[
|
|
"import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk/plugin-entry';",
|
|
"",
|
|
"export default {",
|
|
` id: ${JSON.stringify(pluginId)},`,
|
|
" configSchema: emptyPluginConfigSchema(),",
|
|
" register(api) {",
|
|
" api.registerCommand({",
|
|
" name: 'pair',",
|
|
" description: 'Pair a device',",
|
|
" acceptsArgs: true,",
|
|
" nativeNames: { telegram: 'pair', discord: 'pair' },",
|
|
" async handler({ args }) {",
|
|
" return { text: `paired:${args ?? ''}` };",
|
|
" },",
|
|
" });",
|
|
" },",
|
|
"};",
|
|
"",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
|
|
const runtime = prepareBundledPluginRuntime({ repoRoot });
|
|
try {
|
|
await runtime.publish(() => signal.throwIfAborted());
|
|
} catch (error) {
|
|
try {
|
|
await runtime.cleanup();
|
|
} catch (cleanupError) {
|
|
throw new AggregateError([error, cleanupError], "Smoke runtime setup and cleanup failed.", {
|
|
cause: cleanupError,
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
await runtime.cleanup();
|
|
signal.throwIfAborted();
|
|
|
|
const runtimeEntryPath = path.join(runtimePluginDir, "index.js");
|
|
assert.ok(fs.existsSync(runtimeEntryPath), "runtime overlay entry missing");
|
|
const smsRuntimeEntryPath = path.join(
|
|
repoRoot,
|
|
"dist-runtime",
|
|
"extensions",
|
|
"sms",
|
|
"index.js",
|
|
);
|
|
assert.ok(fs.existsSync(smsRuntimeEntryPath), "compiled SMS runtime entry missing");
|
|
assert.ok(
|
|
fs.existsSync(
|
|
path.join(repoRoot, "dist-runtime", "extensions", "mxc", "mxc-spawn-launcher.mjs"),
|
|
),
|
|
"compiled MXC runtime asset missing",
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(path.join(repoRoot, "dist-runtime", "plugins", "commands.js")),
|
|
false,
|
|
"dist-runtime must not stage a duplicate commands module",
|
|
);
|
|
|
|
clearPluginCommands();
|
|
|
|
const smsStatsBefore = getPluginModuleLoaderStats();
|
|
// Prepared runtimes carry this context into late, plugin-scoped loads. Prove that the load-options
|
|
// projection retains the built-artifact choice instead of reopening source transformation.
|
|
const smsRegistry = loadOpenClawPlugins(
|
|
buildPluginRuntimeLoadOptions(
|
|
resolvePluginRuntimeLoadContext({
|
|
config: {
|
|
plugins: {
|
|
enabled: true,
|
|
allow: ["sms"],
|
|
entries: { sms: { enabled: true } },
|
|
},
|
|
},
|
|
env: {
|
|
...process.env,
|
|
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(repoRoot, "extensions"),
|
|
},
|
|
workspaceDir: tempRoot,
|
|
}),
|
|
{ cache: false, onlyPluginIds: ["sms"] },
|
|
),
|
|
);
|
|
const smsRecord = smsRegistry.plugins.find((entry: { id: string }) => entry.id === "sms");
|
|
assert.ok(smsRecord, "SMS plugin missing from registry");
|
|
assert.equal(smsRecord.status, "loaded", smsRecord.error ?? "SMS plugin failed to load");
|
|
const smsStatsAfter = getPluginModuleLoaderStats();
|
|
assert.ok(
|
|
smsStatsAfter.nativeHits > smsStatsBefore.nativeHits,
|
|
"compiled SMS runtime did not use native loading",
|
|
);
|
|
for (const counter of [
|
|
"nativeMisses",
|
|
"sourceTransformForced",
|
|
"sourceTransformFallbacks",
|
|
] as const) {
|
|
assert.equal(
|
|
smsStatsAfter[counter],
|
|
smsStatsBefore[counter],
|
|
`compiled SMS runtime changed ${counter}`,
|
|
);
|
|
}
|
|
assert.equal(
|
|
smsStatsAfter.topSourceTransformTargets.some(({ target }: { target: string }) =>
|
|
target.replaceAll("\\", "/").includes("/extensions/sms/"),
|
|
),
|
|
false,
|
|
"compiled SMS runtime reached the source transformer",
|
|
);
|
|
|
|
// Exercise the real built load owner with omitted preferences, as provider and
|
|
// tool callers do. Source-owned metadata must not force source execution.
|
|
const { resolvePluginDiscoveryProvidersRuntime } = await import(
|
|
pathToFileURL(path.join(repoRoot, "dist", "plugins", "provider-discovery.runtime.js")).href
|
|
);
|
|
signal.throwIfAborted();
|
|
const { createPluginMetadataSnapshotFixture } =
|
|
await import("../src/plugins/plugin-metadata.test-support.js");
|
|
signal.throwIfAborted();
|
|
const artifactPluginId = "build-artifact-selection";
|
|
const artifactSourceRoot = path.join(tempRoot, "extensions", artifactPluginId);
|
|
const artifactBuiltRoot = path.join(tempRoot, "dist", "extensions", artifactPluginId);
|
|
fs.mkdirSync(artifactSourceRoot, { recursive: true });
|
|
fs.mkdirSync(artifactBuiltRoot, { recursive: true });
|
|
fs.mkdirSync(path.join(artifactSourceRoot, "dist"));
|
|
const artifactEntry = (label: string) => `export default {
|
|
id: ${JSON.stringify(artifactPluginId)},
|
|
register(api) {
|
|
api.registerProvider({ id: ${JSON.stringify(artifactPluginId)}, label: ${JSON.stringify(label)}, auth: [] });
|
|
api.registerTool({ name: "artifact_probe", label: "Artifact", description: ${JSON.stringify(label)},
|
|
parameters: { type: "object", properties: {} },
|
|
async execute() { return { content: [{ type: "text", text: ${JSON.stringify(label)} }] }; }
|
|
});
|
|
}
|
|
};\n`;
|
|
const artifactSource = path.join(artifactSourceRoot, "index.ts");
|
|
fs.writeFileSync(artifactSource, artifactEntry("source"));
|
|
fs.writeFileSync(path.join(artifactBuiltRoot, "index.js"), artifactEntry("compiled"));
|
|
fs.writeFileSync(
|
|
path.join(artifactSourceRoot, "dist", "index.js"),
|
|
artifactEntry("package-local"),
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(artifactSourceRoot, "package.json"),
|
|
JSON.stringify({ type: "module" }),
|
|
);
|
|
for (const [rootDir, extension, label] of [
|
|
[artifactSourceRoot, ".ts", "source"],
|
|
[artifactBuiltRoot, ".js", "compiled"],
|
|
] as const) {
|
|
fs.writeFileSync(
|
|
path.join(rootDir, `provider-discovery${extension}`),
|
|
`export default {
|
|
id: ${JSON.stringify(artifactPluginId)}, label: ${JSON.stringify(label)}, auth: [],
|
|
catalog: { run: async () => ({ providers: {} }) }
|
|
};\n`,
|
|
);
|
|
}
|
|
fs.writeFileSync(
|
|
path.join(artifactBuiltRoot, "package.json"),
|
|
JSON.stringify({
|
|
type: "module",
|
|
openclaw: { extensions: ["./index.js"] },
|
|
}),
|
|
);
|
|
const artifactConfig = {
|
|
plugins: { allow: [artifactPluginId], entries: { [artifactPluginId]: { enabled: true } } },
|
|
};
|
|
const artifactManifest = {
|
|
id: artifactPluginId,
|
|
rootDir: artifactSourceRoot,
|
|
source: artifactSource,
|
|
origin: "bundled" as const,
|
|
channels: [],
|
|
providers: [artifactPluginId],
|
|
cliBackends: [],
|
|
skills: [],
|
|
hooks: [],
|
|
contracts: { tools: ["artifact_probe"] },
|
|
manifestPath: path.join(artifactSourceRoot, "openclaw.plugin.json"),
|
|
providerDiscoverySource: path.join(artifactSourceRoot, "provider-discovery.ts"),
|
|
configSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
packageManifest: { extensions: ["./index.ts"], build: { bundledDist: false } },
|
|
};
|
|
const artifactManifestRegistry = {
|
|
plugins: [artifactManifest],
|
|
diagnostics: [],
|
|
};
|
|
const artifactMetadataSnapshot = createPluginMetadataSnapshotFixture(artifactManifestRegistry);
|
|
for (const toolDiscovery of [false, true]) {
|
|
for (const preferBuiltPluginArtifacts of [undefined, false]) {
|
|
const values = {
|
|
config: artifactConfig,
|
|
env: { ...process.env, OPENCLAW_STATE_DIR: path.join(tempRoot, "artifact-state") },
|
|
manifestRegistry: artifactManifestRegistry,
|
|
installRecords: {},
|
|
preferBuiltPluginArtifacts,
|
|
workspaceDir: tempRoot,
|
|
};
|
|
const options = toolDiscovery
|
|
? buildPluginRuntimeLoadOptions(resolvePluginRuntimeLoadContext(values))
|
|
: values;
|
|
const selected = loadOpenClawPlugins({
|
|
...options,
|
|
cache: false,
|
|
activate: false,
|
|
toolDiscovery,
|
|
onlyPluginIds: [artifactPluginId],
|
|
});
|
|
const label = preferBuiltPluginArtifacts === false ? "source" : "compiled";
|
|
assert.equal(selected.plugins[0]?.status, "loaded", selected.plugins[0]?.error);
|
|
assert.equal(selected.providers[0]?.provider.label, label);
|
|
const tool = selected.tools[0]?.factory({ config: artifactConfig });
|
|
assert.equal(tool?.description, label);
|
|
if (!toolDiscovery) {
|
|
setPluginRuntimeLoadContext(selected, resolvePluginRuntimeLoadContext(values));
|
|
const providers = withPluginRuntimeGenerationScope(
|
|
{ metadataSnapshot: artifactMetadataSnapshot, pluginRegistry: selected },
|
|
() =>
|
|
resolvePluginDiscoveryProvidersRuntime({
|
|
config: artifactConfig,
|
|
pluginMetadataSnapshot: artifactMetadataSnapshot,
|
|
onlyPluginIds: [artifactPluginId],
|
|
discoveryEntriesOnly: true,
|
|
}),
|
|
);
|
|
assert.equal(
|
|
providers[0]?.label,
|
|
label,
|
|
"provider discovery chose a different artifact policy",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Implicit built-host policy leaves installed source alone; explicit true can
|
|
// use its package-local output. Neither cache call order may contaminate the other.
|
|
for (const [orderIndex, preferences] of [
|
|
[undefined, true],
|
|
[true, undefined],
|
|
].entries()) {
|
|
const options = {
|
|
config: artifactConfig,
|
|
env: { ...process.env, OPENCLAW_STATE_DIR: path.join(tempRoot, "artifact-state") },
|
|
workspaceDir: path.join(tempRoot, `cache-order-${orderIndex}`),
|
|
manifestRegistry: {
|
|
...artifactManifestRegistry,
|
|
plugins: [{ ...artifactManifest, origin: "global" }],
|
|
},
|
|
installRecords: {},
|
|
onlyPluginIds: [artifactPluginId],
|
|
cache: true,
|
|
activate: false,
|
|
};
|
|
const registries = preferences.map((preferBuiltPluginArtifacts) => {
|
|
const selected = loadOpenClawPlugins({ ...options, preferBuiltPluginArtifacts });
|
|
const label = preferBuiltPluginArtifacts ? "package-local" : "source";
|
|
assert.equal(selected.plugins[0]?.status, "loaded", selected.plugins[0]?.error);
|
|
assert.equal(
|
|
selected.providers[0]?.provider.label,
|
|
label,
|
|
`cache call order ${orderIndex}`,
|
|
);
|
|
assert.equal(selected.tools[0]?.factory({ config: artifactConfig })?.description, label);
|
|
return selected;
|
|
});
|
|
for (const [index, preferBuiltPluginArtifacts] of preferences.entries()) {
|
|
assert.equal(
|
|
loadOpenClawPlugins({ ...options, preferBuiltPluginArtifacts }),
|
|
registries[index],
|
|
"repeated selection did not reuse its own cached registry",
|
|
);
|
|
}
|
|
}
|
|
|
|
clearPluginCommands();
|
|
|
|
const registry = loadOpenClawPlugins({
|
|
cache: false,
|
|
workspaceDir: tempRoot,
|
|
env: {
|
|
...process.env,
|
|
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(repoRoot, "dist-runtime", "extensions"),
|
|
},
|
|
config: {
|
|
plugins: {
|
|
enabled: true,
|
|
allow: [pluginId],
|
|
entries: {
|
|
[pluginId]: { enabled: true },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const record = registry.plugins.find((entry: { id: string }) => entry.id === pluginId);
|
|
assert.ok(record, "smoke plugin missing from registry");
|
|
assert.equal(record.status, "loaded", record.error ?? "smoke plugin failed to load");
|
|
|
|
assert.deepEqual(
|
|
getPluginCommandSpecs().filter((command: { name: string }) => command.name === "pair"),
|
|
[{ name: "pair", description: "Pair a device", acceptsArgs: true }],
|
|
);
|
|
|
|
const match = matchPluginCommand("/pair now");
|
|
assert.ok(match, "canonical built command registry did not receive the command");
|
|
assert.equal(match.args, "now");
|
|
const result = await match.command.handler({ args: match.args });
|
|
signal.throwIfAborted();
|
|
assert.deepEqual(result, { text: "paired:now" });
|
|
|
|
// Keep these imports after the cold native checks so they cannot prewarm the loader.
|
|
const { buildBundleMcpToolsFromCatalog } = await import(
|
|
pathToFileURL(path.join(repoRoot, "dist", "agents", "agent-bundle-mcp-materialize.js")).href
|
|
);
|
|
signal.throwIfAborted();
|
|
const { getPluginToolMeta } = await import(
|
|
pathToFileURL(path.join(repoRoot, "dist", "plugins", "tool-metadata.js")).href
|
|
);
|
|
signal.throwIfAborted();
|
|
const { getPluginToolMeta: getSdkPluginToolMeta } = await import(
|
|
pathToFileURL(path.join(repoRoot, "dist", "plugin-sdk", "agent-harness-runtime.js")).href
|
|
);
|
|
signal.throwIfAborted();
|
|
const [mcpTool] = buildBundleMcpToolsFromCatalog({
|
|
catalog: {
|
|
version: 1,
|
|
generatedAt: 0,
|
|
servers: {
|
|
"build-smoke-mcp": {
|
|
serverName: "build-smoke-mcp",
|
|
safeServerName: "build-smoke-mcp",
|
|
launchSummary: "build smoke inventory fixture",
|
|
toolCount: 1,
|
|
},
|
|
},
|
|
tools: [
|
|
{
|
|
serverName: "build-smoke-mcp",
|
|
safeServerName: "build-smoke-mcp",
|
|
toolName: "lookup",
|
|
inputSchema: { type: "object", properties: {} },
|
|
fallbackDescription: "Look up a build smoke inventory item",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
assert.ok(mcpTool, "compiled MCP materializer did not produce a tool");
|
|
const mcpMetadata = getPluginToolMeta(mcpTool);
|
|
assert.ok(mcpMetadata, "canonical built metadata owner did not receive MCP tool metadata");
|
|
assert.equal(mcpMetadata.pluginId, "bundle-mcp");
|
|
assert.equal(mcpMetadata.mcp?.serverName, "build-smoke-mcp");
|
|
assert.equal(mcpMetadata.mcp?.safeServerName, "build-smoke-mcp");
|
|
assert.equal(mcpMetadata.mcp?.toolName, "lookup");
|
|
assert.equal(mcpMetadata.mcp?.operation, "tool");
|
|
assert.strictEqual(
|
|
getSdkPluginToolMeta(mcpTool),
|
|
mcpMetadata,
|
|
"public agent-harness-runtime SDK did not read the canonical MCP metadata record",
|
|
);
|
|
} finally {
|
|
process.off("exit", cleanup);
|
|
cleanup();
|
|
}
|
|
signal.throwIfAborted();
|
|
const remaining = prepareBundledPluginRuntime({ repoRoot });
|
|
const changed = remaining.changed;
|
|
await remaining.cleanup();
|
|
assert.equal(changed, false, "singleton smoke left noncanonical runtime artifacts");
|
|
process.stdout.write("[build-smoke] built plugin singleton smoke passed\n");
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
let exitCode = 0;
|
|
const cancel = (code: number) => {
|
|
if (!controller.signal.aborted) {
|
|
exitCode = code;
|
|
controller.abort(new Error("Built plugin singleton smoke canceled."));
|
|
}
|
|
};
|
|
const onSigint = () => cancel(130);
|
|
const onSigterm = () => cancel(143);
|
|
try {
|
|
await withDistArtifactOwnership(repoRoot, async () => {
|
|
process.on("SIGINT", onSigint);
|
|
process.on("SIGTERM", onSigterm);
|
|
// Imports cannot be interrupted; keep ownership until each pending read joins.
|
|
await runBuiltPluginSingletonSmoke(controller.signal);
|
|
});
|
|
controller.signal.throwIfAborted();
|
|
} catch (error) {
|
|
if (!controller.signal.aborted || error !== controller.signal.reason) {
|
|
throw error;
|
|
}
|
|
process.exitCode = exitCode;
|
|
} finally {
|
|
process.off("SIGINT", onSigint);
|
|
process.off("SIGTERM", onSigterm);
|
|
}
|