diff --git a/README.md b/README.md index dd060fe..1902246 100644 --- a/README.md +++ b/README.md @@ -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 - **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) diff --git a/crabpot.config.json b/crabpot.config.json index 634b4f8..7605443 100644 --- a/crabpot.config.json +++ b/crabpot.config.json @@ -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", diff --git a/crabpot.schema.json b/crabpot.schema.json index f180412..6233be5 100644 --- a/crabpot.schema.json +++ b/crabpot.schema.json @@ -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": { diff --git a/scripts/manifest-lib.mjs b/scripts/manifest-lib.mjs index f7059f5..918f2cd 100644 --- a/scripts/manifest-lib.mjs +++ b/scripts/manifest-lib.mjs @@ -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)) { diff --git a/scripts/package-availability.mjs b/scripts/package-availability.mjs new file mode 100644 index 0000000..4035381 --- /dev/null +++ b/scripts/package-availability.mjs @@ -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"); +} diff --git a/scripts/report-lib.mjs b/scripts/report-lib.mjs index 0f7e141..80a9598 100644 --- a/scripts/report-lib.mjs +++ b/scripts/report-lib.mjs @@ -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("\\", "/"); } diff --git a/scripts/run-static-suite.mjs b/scripts/run-static-suite.mjs index 92da6fd..7333bb6 100644 --- a/scripts/run-static-suite.mjs +++ b/scripts/run-static-suite.mjs @@ -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], diff --git a/scripts/sync-fixtures.mjs b/scripts/sync-fixtures.mjs index cdc1122..56e91e6 100644 --- a/scripts/sync-fixtures.mjs +++ b/scripts/sync-fixtures.mjs @@ -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)}`); diff --git a/scripts/update-readme-summary.mjs b/scripts/update-readme-summary.mjs index 0c31475..bc96459 100644 --- a/scripts/update-readme-summary.mjs +++ b/scripts/update-readme-summary.mjs @@ -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"); } diff --git a/scripts/update-track-metadata.mjs b/scripts/update-track-metadata.mjs index f985429..6ddabf1 100644 --- a/scripts/update-track-metadata.mjs +++ b/scripts/update-track-metadata.mjs @@ -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) { diff --git a/test/manifest.test.mjs b/test/manifest.test.mjs index 1b06d1c..95a3412 100644 --- a/test/manifest.test.mjs +++ b/test/manifest.test.mjs @@ -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", diff --git a/test/report.test.mjs b/test/report.test.mjs index 1ff0922..c368cbc 100644 --- a/test/report.test.mjs +++ b/test/report.test.mjs @@ -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: { diff --git a/test/run-static-suite.test.mjs b/test/run-static-suite.test.mjs index 729b4fa..72c05eb 100644 --- a/test/run-static-suite.test.mjs +++ b/test/run-static-suite.test.mjs @@ -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"),