fix(reports): flag missing externalized OpenClaw npm artifacts

Handle missing npm artifacts as P0s for externalized OpenClaw fixtures while source-packing bundled Matrix/Mattermost from the resolved OpenClaw checkout.
This commit is contained in:
Vincent Koc 2026-05-04 20:21:43 -07:00 committed by GitHub
parent 92ff0d2fc6
commit d68c82e8d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 374 additions and 29 deletions

View File

@ -9,14 +9,14 @@
## Reporting Data
`main` follows the latest published npm package and npm `latest` plugin artifacts. `crab-beta` follows beta npm dist-tags. `crab-development` checks `openclaw/openclaw` main against source-packed official plugin artifacts from that same OpenClaw checkout.
`main` follows the latest published npm package and npm `latest` plugin artifacts, with bundled OpenClaw fixtures source-packed from the matching checkout. `crab-beta` follows beta npm dist-tags for externalized packages and source-packs bundled fixtures. `crab-development` checks `openclaw/openclaw` main against source-packed official plugin artifacts from that same OpenClaw checkout.
- **Last dashboard update:** May 05, 2026, 02:45 UTC
<!-- crabpot-tracks:start -->
- **Source:** `npm-latest`
- **OpenClaw version:** `2026.5.3-1`
- **OpenClaw SHA:** `2eae30e779cb`
- **Dashboard target:** `openclaw@latest + @openclaw/*@latest`
- **Plugin artifacts:** `npm latest fixture set`
- **Dashboard target:** `openclaw@latest + @openclaw/*@latest + bundled source fixtures`
- **Plugin artifacts:** `npm latest fixture set plus bundled source-packed fixtures`
- **GitHub report run:** [25354973898](https://github.com/openclaw/crabpot/actions/runs/25354973898)
<!-- crabpot-tracks:end -->

View File

@ -459,7 +459,8 @@
"name": "OpenClaw Matrix channel plugin",
"package": {
"name": "@openclaw/matrix",
"tag": "latest"
"tag": "latest",
"artifactSource": "source-pack"
},
"source": {
"repo": "https://github.com/openclaw/openclaw.git",
@ -472,15 +473,14 @@
"channel",
"gateway-method",
"subagent-routing",
"cli",
"npm-artifact"
"cli"
],
"expect": {
"registrations": [
"registerChannel"
]
},
"why": "Official OpenClaw Matrix channel package covering CLI setup, gateway methods, subagent routing hooks, and monorepo-backed npm packaging."
"why": "Official bundled OpenClaw Matrix channel package covering CLI setup, gateway methods, and subagent routing hooks; Crabpot source-packs it from the OpenClaw monorepo instead of requiring a separate npm dist-tag."
},
{
"id": "msteams",
@ -745,7 +745,8 @@
"name": "OpenClaw Mattermost channel plugin",
"package": {
"name": "@openclaw/mattermost",
"tag": "latest"
"tag": "latest",
"artifactSource": "source-pack"
},
"source": {
"repo": "https://github.com/openclaw/openclaw.git",
@ -758,15 +759,14 @@
"channel",
"http-routes",
"self-hosted-chat",
"account-auth",
"npm-artifact"
"account-auth"
],
"expect": {
"registrations": [
"registerChannel"
]
},
"why": "Official Mattermost channel package covering self-hosted chat auth, HTTP route registration, channel factory metadata, and npm artifact packaging."
"why": "Official bundled Mattermost channel package covering self-hosted chat auth, HTTP route registration, and channel factory metadata; Crabpot source-packs it from the OpenClaw monorepo instead of requiring a separate npm dist-tag."
},
{
"id": "synology-chat",

View File

@ -34,7 +34,8 @@
"properties": {
"name": { "type": "string", "minLength": 1 },
"version": { "type": "string", "minLength": 1 },
"tag": { "type": "string", "minLength": 1 }
"tag": { "type": "string", "minLength": 1 },
"artifactSource": { "enum": ["npm", "source-pack"] }
}
},
"source": {

View File

@ -101,6 +101,15 @@ export function validateManifest(manifest) {
if (fixture.package.version !== undefined && (typeof fixture.package.version !== "string" || fixture.package.version.length === 0)) {
errors.push(`${fixture.id}: package.version must be a non-empty string when present`);
}
if (
fixture.package.artifactSource !== undefined &&
!["npm", "source-pack"].includes(fixture.package.artifactSource)
) {
errors.push(`${fixture.id}: package.artifactSource must be npm or source-pack when present`);
}
if (fixture.package.artifactSource === "source-pack" && fixture.source === undefined) {
errors.push(`${fixture.id}: package.artifactSource source-pack requires source metadata`);
}
}
if (fixture.source !== undefined) {
if (!fixture.source || typeof fixture.source !== "object" || Array.isArray(fixture.source)) {

View File

@ -0,0 +1,127 @@
import { existsSync, readFileSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { repoRoot } from "./manifest-lib.mjs";
export const defaultPackageAvailabilityPath = path.join(
repoRoot,
"reports/crabpot-package-availability.json",
);
export function packageAvailabilityCode() {
return "package-npm-pack-unavailable";
}
export function readPackageAvailabilityReport(reportPath = defaultPackageAvailabilityPath) {
if (!existsSync(reportPath)) {
return null;
}
return JSON.parse(readFileSync(reportPath, "utf8"));
}
export async function writePackageAvailabilityReport(report, reportPath = defaultPackageAvailabilityPath) {
await mkdir(path.dirname(reportPath), { recursive: true });
await writeFile(reportPath, `${JSON.stringify(normalizePackageAvailabilityReport(report), null, 2)}\n`, "utf8");
}
export function normalizePackageAvailabilityReport(report = {}) {
const failures = Array.isArray(report.failures) ? report.failures : [];
return {
generatedAt: report.generatedAt ?? "deterministic",
fixtureSet: report.fixtureSet ?? "all",
pluginTrack: report.pluginTrack ?? "manifest",
summary: {
failureCount: failures.length,
openclawFailureCount: failures.filter((failure) => failure.openclawPackage).length,
fallbackCount: failures.filter((failure) => failure.fallbackVersion).length,
},
failures,
};
}
export function packageAvailabilityActionableFailures(report, options = {}) {
const sourcePackFixtures = sourcePackFixtureIds(options.manifest);
return (report?.failures ?? [])
.filter((failure) => failure.openclawPackage)
.filter((failure) => failure.artifactSource !== "source-pack")
.filter((failure) => !sourcePackFixtures.has(failure.fixture));
}
export function packageAvailabilityIssues(report, options = {}) {
return packageAvailabilityActionableFailures(report, options)
.map((failure) => ({
id: issueIdForPackageFailure(failure),
fixture: failure.fixture,
severity: "P0",
owner: "plugin",
code: packageAvailabilityCode(),
decision: "plugin-release-fix",
status: "blocking",
issueClass: "live-issue",
live: true,
deprecated: false,
compatStatus: "none",
title: `${failure.fixture}: OpenClaw npm artifact is unavailable for ${failure.requestedTag ?? "requested track"}`,
evidence: [
`${failure.packageName}@${failure.requestedTag ?? failure.requestedVersion ?? "unknown"}`,
failure.message,
...(failure.fallbackVersion
? [`fallback:${failure.packageName}@${failure.fallbackVersion}`]
: []),
].filter(Boolean),
compatRecord: null,
runtimeCoverage: null,
}));
}
export function packageAvailabilityDecisions(report, options = {}) {
return packageAvailabilityActionableFailures(report, options)
.map((failure) => ({
fixture: failure.fixture,
decision: "plugin-release-fix",
seam: "npm-artifact",
action: `Restore the OpenClaw npm artifact for ${failure.packageName}@${failure.requestedTag ?? failure.requestedVersion ?? "requested track"} before trusting this track as release-complete.`,
evidence: failure.message,
}));
}
function sourcePackFixtureIds(manifest) {
return new Set(
(manifest?.fixtures ?? [])
.filter((fixture) => fixture.package?.artifactSource === "source-pack")
.map((fixture) => fixture.id),
);
}
export function mergePackageAvailabilityIntoSummary(summary, issues) {
const openIssues = issues.filter((issue) => issue.status !== "runtime-covered");
return {
...summary,
issueCount: issues.length,
openIssueCount: openIssues.length,
p0IssueCount: issues.filter((issue) => issue.severity === "P0").length,
openP0IssueCount: openIssues.filter((issue) => issue.severity === "P0").length,
liveIssueCount: issues.filter((issue) => issue.issueClass === "live-issue").length,
liveP0IssueCount: issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity === "P0").length,
};
}
function issueIdForPackageFailure(failure) {
return `CRABPOT-${stableHash([
failure.fixture,
packageAvailabilityCode(),
failure.packageName,
failure.requestedTag ?? "",
failure.requestedVersion ?? "",
failure.message,
].join("\n"))}`;
}
function stableHash(value) {
let hash = 2166136261;
for (let index = 0; index < value.length; index += 1) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0).toString(16).toUpperCase().padStart(8, "0");
}

View File

@ -3,6 +3,14 @@ import { existsSync, readFileSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { readConfiguredManifest, repoRoot } from "./manifest-lib.mjs";
import {
defaultPackageAvailabilityPath,
packageAvailabilityActionableFailures,
mergePackageAvailabilityIntoSummary,
packageAvailabilityDecisions,
packageAvailabilityIssues,
readPackageAvailabilityReport,
} from "./package-availability.mjs";
import { loadPluginInspectorPublicApi } from "./plugin-inspector-source.mjs";
import { defaultExecutionResultsJsonPath } from "./summarize-execution-results.mjs";
@ -24,6 +32,8 @@ export async function buildReport(options = {}) {
const manifest = await readConfiguredManifest({ fixtureSet: options.fixtureSet });
const executionResults =
options.executionResults ?? readOptionalExecutionResults(options.executionResultsPath);
const packageAvailability =
options.packageAvailability ?? readDefaultPackageAvailability(options.packageAvailabilityPath);
const report = await pluginInspector.inspectCompatibilityFixtureSetConfig({
config: {
...manifest,
@ -33,13 +43,22 @@ export async function buildReport(options = {}) {
executionResults,
openclawPath: options.openclawPath,
});
const packageIssues = packageAvailabilityIssues(packageAvailability, { manifest });
const packageDecisions = packageAvailabilityDecisions(packageAvailability, { manifest });
const issues = [...packageIssues, ...(report.issues ?? [])];
return {
...report,
summary: mergePackageAvailabilityIntoSummary(report.summary, issues),
issues,
decisions: [...packageDecisions, ...(report.decisions ?? [])],
crabpotContext: buildReportContext({
executionResults,
executionResultsPath: options.executionResultsPath,
manifest,
openclawPath: options.openclawPath,
packageAvailability,
packageAvailabilityActionableFailures: packageAvailabilityActionableFailures(packageAvailability, { manifest }),
packageAvailabilityPath: options.packageAvailabilityPath,
}),
};
}
@ -61,7 +80,15 @@ export async function writeReport(report, options = {}) {
return { markdownPath, jsonPath, issuesPath };
}
function buildReportContext({ executionResults, executionResultsPath, manifest, openclawPath }) {
function buildReportContext({
executionResults,
executionResultsPath,
manifest,
openclawPath,
packageAvailability,
packageAvailabilityActionableFailures,
packageAvailabilityPath,
}) {
return {
fixtureSet: manifest.fixtureSelection?.fixtureSet ?? "all",
fixtureIds: manifest.fixtureSelection?.ids ?? manifest.fixtures.map((fixture) => fixture.id),
@ -75,6 +102,14 @@ function buildReportContext({ executionResults, executionResultsPath, manifest,
capturedRegistrations: executionResults.summary?.capturedRegistrationCount ?? 0,
}
: null,
packageAvailability: packageAvailability
? {
path: packageAvailabilityPath ?? defaultPackageAvailabilityPath,
failures: packageAvailability.summary?.failureCount ?? packageAvailability.failures?.length ?? 0,
openclawFailures: packageAvailabilityActionableFailures?.length ?? 0,
fallbacks: packageAvailability.summary?.fallbackCount ?? 0,
}
: null,
};
}
@ -118,6 +153,11 @@ function withReportContext(report, markdown) {
`- **Runtime evidence:** \`${relativePathLabel(context.runtimeEvidence.path)}\` (${context.runtimeEvidence.captureArtifacts} capture artifacts, ${context.runtimeEvidence.capturedRegistrations} captured registrations/hooks)`,
]
: []),
...(context.packageAvailability
? [
`- **Package availability:** \`${relativePathLabel(context.packageAvailability.path)}\` (${context.packageAvailability.openclawFailures} OpenClaw failures, ${context.packageAvailability.fallbacks} fallbacks)`,
]
: []),
"",
].join("\n");
const firstSection = markdown.indexOf("\n## ");
@ -138,6 +178,16 @@ function readOptionalExecutionResults(executionResultsPath) {
return JSON.parse(readFileSync(resolvedPath, "utf8"));
}
function readDefaultPackageAvailability(packageAvailabilityPath) {
if (packageAvailabilityPath) {
return readPackageAvailabilityReport(packageAvailabilityPath);
}
if (!process.env.CRABPOT_PLUGIN_TRACK && !process.env.CRABPOT_PACKAGE_AVAILABILITY_PATH) {
return null;
}
return readPackageAvailabilityReport(process.env.CRABPOT_PACKAGE_AVAILABILITY_PATH);
}
function relativePathLabel(filePath) {
return path.relative(repoRoot, path.resolve(repoRoot, filePath)).replaceAll("\\", "/");
}

View File

@ -42,10 +42,10 @@ export function buildStaticSuiteSteps({
} = {}) {
return [
["node", ["scripts/check-openclaw-plugin-contracts.mjs"]],
["node", ["scripts/sync-fixtures.mjs", "--materialize"]],
["node", ["scripts/sync-fixtures.mjs", "--materialize", ...openclawArgs]],
["node", ["--test", "test/*.test.mjs"]],
...(Object.keys(fixtureEnv).length > 0
? [["node", ["scripts/sync-fixtures.mjs", "--materialize"], fixtureEnv]]
? [["node", ["scripts/sync-fixtures.mjs", "--materialize", ...openclawArgs], fixtureEnv]]
: []),
["node", ["scripts/sync-fixtures.mjs", "--check"], fixtureEnv],
["node", ["scripts/run-contract-smoke.mjs", "--strict", ...openclawArgs], fixtureEnv],

View File

@ -5,6 +5,7 @@ import os from "node:os";
import { spawnSync } from "node:child_process";
import path from "node:path";
import { fixtureSourceRoot, readConfiguredManifest, repoRoot } from "./manifest-lib.mjs";
import { writePackageAvailabilityReport } from "./package-availability.mjs";
const openclawSourceRepo = "https://github.com/openclaw/openclaw.git";
const sourcePackPluginTrack = "source-pack";
@ -14,6 +15,7 @@ const materialize = args.materialize;
const check = args.check || !materialize;
const manifest = await readConfiguredManifest({ fixtureSet: args.fixtureSet });
const packageAvailabilityFailures = [];
if (check) {
await checkGitmodules(manifest);
@ -44,6 +46,13 @@ for (const fixture of manifest.fixtures) {
run("git", ["-c", "safe.directory=*", "submodule", "add", "--depth", "1", fixture.repo, fixture.path]);
}
await writePackageAvailabilityReport({
generatedAt: new Date().toISOString(),
fixtureSet: args.fixtureSet || "all",
pluginTrack: args.pluginTrack || "manifest",
failures: packageAvailabilityFailures,
});
console.log("crabpot: fixtures materialized. review .gitmodules and commit pinned revisions.");
async function checkGitmodules(manifest) {
@ -71,6 +80,9 @@ async function checkGitmodules(manifest) {
async function checkNpmFixtureShims(manifest) {
const errors = [];
for (const fixture of manifest.fixtures.filter((item) => item.package)) {
if (packageArtifactSource(fixture) === sourcePackPluginTrack) {
continue;
}
try {
await readNpmFixtureDependency(fixture);
} catch (error) {
@ -83,7 +95,9 @@ async function checkNpmFixtureShims(manifest) {
}
async function materializeNpmFixture(fixture, target) {
const dependency = await resolveNpmFixtureDependency(fixture, { pluginTrack: args.pluginTrack });
const dependency = await resolveNpmFixtureDependencyWithFallback(fixture, {
pluginTrack: args.pluginTrack,
});
const spec = `${dependency.name}@${dependency.version}`;
const tempDir = await mkdtemp(path.join(os.tmpdir(), "crabpot-npm-fixture-"));
const payloadDir = fixtureSourceRoot(fixture);
@ -98,7 +112,17 @@ async function materializeNpmFixture(fixture, target) {
if (pack.status !== 0) {
process.stderr.write(pack.stderr ?? "");
const detail = pack.error ? `: ${pack.error.message}` : "";
throw new Error(`npm pack ${spec} failed with ${pack.status}${detail}`);
recordPackageAvailabilityFailure(fixture, {
fallbackVersion: dependency.fallbackVersion,
message: `npm pack ${spec} failed with ${pack.status}${detail}`,
requestedTag: dependency.requestedTag,
requestedVersion: dependency.version,
reason: "npm-pack-failed",
});
if (!existsSync(payloadDir)) {
await mkdir(payloadDir, { recursive: true });
}
return;
}
const packed = JSON.parse(pack.stdout)[0];
if (!packed?.filename) {
@ -192,6 +216,31 @@ async function resolveNpmFixtureDependency(fixture, options = {}) {
};
}
async function resolveNpmFixtureDependencyWithFallback(fixture, options = {}) {
try {
return await resolveNpmFixtureDependency(fixture, options);
} catch (error) {
const declared = await readNpmFixtureDependency(fixture);
const requestedTag = selectedPackageTag(fixture, options.pluginTrack);
recordPackageAvailabilityFailure(fixture, {
fallbackVersion: declared.version,
message: error.message,
requestedTag,
requestedVersion: declared.version,
reason: "npm-dist-tag-missing",
});
console.warn(
`crabpot: ${fixture.id} ${fixture.package.name}@${requestedTag} unavailable; falling back to pinned ${declared.version}`,
);
return {
...declared,
fallbackVersion: declared.version,
requestedTag,
tag: "",
};
}
}
async function readNpmFixtureDependency(fixture) {
const shimPath = path.join(repoRoot, fixture.path, "package.json");
if (!existsSync(shimPath)) {
@ -215,7 +264,11 @@ async function readNpmFixtureDependency(fixture) {
}
function shouldMaterializeSourcePack(fixture, pluginTrack) {
if (pluginTrack !== sourcePackPluginTrack || !isOpenClawPackage(fixture.package.name)) {
const artifactSource = packageArtifactSource(fixture);
if (
artifactSource !== sourcePackPluginTrack &&
(pluginTrack !== sourcePackPluginTrack || !isOpenClawPackage(fixture.package.name))
) {
return false;
}
if (!fixture.source) {
@ -228,7 +281,10 @@ function shouldMaterializeSourcePack(fixture, pluginTrack) {
}
function selectedPackageTag(fixture, pluginTrack) {
if (pluginTrack === sourcePackPluginTrack && isOpenClawPackage(fixture.package.name)) {
if (
packageArtifactSource(fixture) === sourcePackPluginTrack ||
(pluginTrack === sourcePackPluginTrack && isOpenClawPackage(fixture.package.name))
) {
return "";
}
if (pluginTrack && pluginTrack !== "manifest" && isOpenClawPackage(fixture.package.name)) {
@ -244,6 +300,10 @@ function isOpenClawPackage(name) {
return /^@openclaw\//.test(name);
}
function packageArtifactSource(fixture) {
return fixture.package?.artifactSource ?? "npm";
}
async function npmDistTag(name, tag) {
const result = spawnSync("npm", ["view", name, "dist-tags", "--json"], {
cwd: repoRoot,
@ -263,6 +323,21 @@ async function npmDistTag(name, tag) {
return version;
}
function recordPackageAvailabilityFailure(fixture, failure) {
packageAvailabilityFailures.push({
fixture: fixture.id,
packageName: fixture.package.name,
requestedTag: failure.requestedTag || null,
requestedVersion: failure.requestedVersion || null,
fallbackVersion: failure.fallbackVersion || null,
openclawPackage: isOpenClawPackage(fixture.package.name),
artifactSource: packageArtifactSource(fixture),
reason: failure.reason,
message: failure.message,
path: fixture.path,
});
}
async function npmPackageGitHead(name, version) {
const result = spawnSync("npm", ["view", `${name}@${version}`, "gitHead", "--json"], {
cwd: repoRoot,
@ -358,11 +433,12 @@ function run(command, args) {
}
function resolveOpenClawSourceRoot() {
if (!args.openclawPath) {
throw new Error("source-pack requires --openclaw or CRABPOT_TEST_OPENCLAW_PATH");
const configuredPath = args.openclawPath || manifest.openclaw?.defaultCheckoutPath || "";
if (!configuredPath) {
throw new Error("source-pack requires --openclaw, CRABPOT_TEST_OPENCLAW_PATH, or openclaw.defaultCheckoutPath");
}
const root = path.resolve(repoRoot, args.openclawPath);
const root = path.resolve(repoRoot, configuredPath);
const packageJsonPath = path.join(root, "package.json");
if (!existsSync(packageJsonPath)) {
throw new Error(`source-pack OpenClaw checkout is missing package.json: ${path.relative(repoRoot, packageJsonPath)}`);

View File

@ -379,7 +379,7 @@ function renderReadmeFrame(summary) {
"",
"## Reporting Data",
"",
"`main` follows the latest published npm package and npm `latest` plugin artifacts. `crab-beta` follows beta npm dist-tags. `crab-development` checks `openclaw/openclaw` main against source-packed official plugin artifacts from that same OpenClaw checkout.",
"`main` follows the latest published npm package and npm `latest` plugin artifacts, with bundled OpenClaw fixtures source-packed from the matching checkout. `crab-beta` follows beta npm dist-tags for externalized packages and source-packs bundled fixtures. `crab-development` checks `openclaw/openclaw` main against source-packed official plugin artifacts from that same OpenClaw checkout.",
`- **Last dashboard update:** ${summary.generatedAtLabel ?? formatTimestamp(summary.generatedAt)}`,
].join("\n");
}

View File

@ -17,15 +17,15 @@ const branchUrls = {
};
const trackTargets = {
beta: "openclaw@beta + @openclaw/*@beta",
beta: "openclaw@beta + @openclaw/*@beta + bundled source fixtures",
development: "openclaw/openclaw@main + source-packed @openclaw/*",
latest: "openclaw@latest + @openclaw/*@latest",
latest: "openclaw@latest + @openclaw/*@latest + bundled source fixtures",
};
const pluginArtifacts = {
beta: "npm beta fixture set",
beta: "npm beta fixture set plus bundled source-packed fixtures",
development: "source-packed from OpenClaw checkout",
latest: "npm latest fixture set",
latest: "npm latest fixture set plus bundled source-packed fixtures",
};
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {

View File

@ -51,6 +51,19 @@ test("openclaw beta fixture set narrows to beta npm packages", async () => {
assert.ok(manifest.fixtures.every((fixture) => fixture.package?.tag === "beta"));
});
test("bundled OpenClaw channels source-pack from the monorepo", async () => {
const manifest = await readManifest();
const bundled = manifest.fixtures.filter((fixture) => ["matrix", "mattermost"].includes(fixture.id));
assert.deepEqual(bundled.map((fixture) => fixture.id), ["matrix", "mattermost"]);
for (const fixture of bundled) {
assert.equal(fixture.package.artifactSource, "source-pack");
assert.ok(fixture.source.path.startsWith("extensions/"));
assert.equal(fixture.seams.includes("npm-artifact"), false);
assert.match(fixture.why, /source-packs it from the OpenClaw monorepo/);
}
});
test("explicit fixture set narrows to named fixtures", async () => {
const manifest = await readConfiguredManifest({ fixtureSet: "kitchen-sink,wecom" });
@ -81,6 +94,7 @@ test("manifest validation rejects invalid fixture contracts before CI materializ
"fixture must declare exactly one of repo or package",
"repo must be a GitHub HTTPS .git URL",
"package.name must be set",
"package.artifactSource must be npm or source-pack when present",
"source.repo must be a GitHub HTTPS .git URL",
"source.path must be a repo-relative path",
"source.ref must be set",
@ -104,7 +118,7 @@ function invalidManifest() {
id: "Bad_ID",
path: "../outside",
repo: "git@github.com:owner/repo",
package: {},
package: { artifactSource: "registry" },
source: {
repo: "git@github.com:owner/repo",
path: "../outside",

View File

@ -134,6 +134,74 @@ test("report can focus on the OpenClaw beta npm fixture set", async () => {
assert.ok(report.fixtures.every((fixture) => report.crabpotContext.fixtureIds.includes(fixture.id)));
});
test("OpenClaw npm artifact availability failures become P0 live issues", async () => {
const report = await buildReport({
generatedAt: "test",
openclawPath: false,
packageAvailability: {
generatedAt: "test",
fixtureSet: "openclaw-beta",
pluginTrack: "beta",
summary: {
failureCount: 3,
openclawFailureCount: 2,
fallbackCount: 2,
},
failures: [
{
fixture: "mattermost",
packageName: "@openclaw/mattermost",
requestedTag: "beta",
requestedVersion: "2026.2.21",
fallbackVersion: "2026.2.21",
openclawPackage: true,
reason: "npm-dist-tag-missing",
message: "@openclaw/mattermost: npm dist-tag beta resolved to invalid version undefined",
path: "plugins/mattermost",
},
{
fixture: "whatsapp",
packageName: "@openclaw/whatsapp",
requestedTag: "beta",
requestedVersion: "2026.2.21",
fallbackVersion: "2026.2.21",
openclawPackage: true,
reason: "npm-dist-tag-missing",
message: "@openclaw/whatsapp: npm dist-tag beta resolved to invalid version undefined",
path: "plugins/whatsapp",
},
{
fixture: "external",
packageName: "external-plugin",
requestedTag: "beta",
openclawPackage: false,
reason: "npm-dist-tag-missing",
message: "external-plugin: npm dist-tag beta resolved to invalid version undefined",
path: "plugins/external",
},
],
},
});
const issue = report.issues.find(
(candidate) =>
candidate.fixture === "whatsapp" &&
candidate.code === "package-npm-pack-unavailable",
);
const markdown = renderMarkdownReport(report);
assert.equal(issue.severity, "P0");
assert.equal(issue.issueClass, "live-issue");
assert.equal(issue.status, "blocking");
assert.equal(issue.decision, "plugin-release-fix");
assert.ok(report.summary.p0IssueCount >= 1);
assert.equal(report.issues.filter((candidate) => candidate.code === "package-npm-pack-unavailable").length, 1);
assert.equal(report.crabpotContext.packageAvailability.openclawFailures, 1);
assert.match(markdown, /Package availability/);
assert.match(markdown, /@openclaw\/whatsapp@beta/);
assert.doesNotMatch(markdown, /@openclaw\/mattermost@beta/);
assert.doesNotMatch(markdown, /external-plugin/);
});
test("report can reconcile runtime execution evidence", async () => {
const report = await buildReport({
executionResults: {

View File

@ -13,7 +13,7 @@ test("static suite keeps the dashboard gate broad and target-explicit", () => {
const rendered = steps.map(([command, args]) => [command, args.join(" ")]);
assert.deepEqual(rendered[0], ["node", "scripts/check-openclaw-plugin-contracts.mjs"]);
assert.deepEqual(rendered[1], ["node", "scripts/sync-fixtures.mjs --materialize"]);
assert.deepEqual(rendered[1], ["node", "scripts/sync-fixtures.mjs --materialize --openclaw ./openclaw"]);
assert.ok(rendered.some(([command, args]) => command === "node" && args === "--test test/*.test.mjs"));
assert.ok(
rendered.some(([command, args]) => command === "node" && args === "scripts/run-plugin-inspector-smoke.mjs --check"),