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:
parent
92ff0d2fc6
commit
d68c82e8d7
@ -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 -->
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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)) {
|
||||
|
||||
127
scripts/package-availability.mjs
Normal file
127
scripts/package-availability.mjs
Normal 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");
|
||||
}
|
||||
@ -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("\\", "/");
|
||||
}
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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)}`);
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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"),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user