Skip to content

Add package ecosystem smoke harness and SDK test helper - #108

Merged
unbraind merged 4 commits into
mainfrom
package-ecosystem-smoke-sdk-helper
Jun 4, 2026
Merged

unbraind merged 4 commits into
mainfrom
package-ecosystem-smoke-sdk-helper

Conversation

@unbraind

@unbraind unbraind commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add a reusable external npm package smoke harness for isolated temp-project package install, package doctor, and runtime contract probes.
  • Expose activateExtensionForTest from the public SDK testing surface and document the package-author testing pattern.
  • Refresh package-owned CHANGELOG.md with pm-changelog 2026.6.4 and update the Sentry gate allowlist for handled one-error validation CommandErrors.

pm tracking

  • Reused and closed pm-vnjh under pm-ugqx; no duplicate pm item created.
  • Sidecar GitHub/release/security triage evidence recorded on pm-u9d0.
  • SDK sidecar evidence recorded on pm-ugqx and folded into pm-vnjh.

Verification

  • pm test pm-vnjh --run --progress passed 11/11 linked commands.
  • node scripts/run-tests.mjs coverage: 136 files, 2216 tests, 100% statements/branches/functions/lines.
  • pnpm typecheck, pnpm build, pnpm quality:static, pnpm quality:docs-skills, pnpm security:scan.
  • pnpm smoke:external-packages -- --package pm-changelog passed in isolated temp project.
  • node scripts/release/sentry-telemetry-gate.mjs --json passed with critical=0 high=0.
  • bash scripts/prod/telemetry/check-stack-health.sh passed; API ok and recent event volume present.
  • pnpm changelog:pm:check passed with pm-changelog 2026.6.4.

Notes

  • Auto Release is still blocked operationally when push=true until the release environment has SENTRY_PERSONAL_ADMIN_TOKEN configured; this branch verifies the Sentry gate itself works when the token is available.

Implement pm-vnjh by adding a reusable external npm package smoke harness that discovers pm-package npm modules, installs each in isolated PM_PATH/PM_GLOBAL_PATH temp projects, runs package doctor, and probes runtime contracts.

Expose activateExtensionForTest through the public SDK testing surface so package authors can exercise the real extension activation and capability guardrails without private loader imports.

Document the package-author and ecosystem smoke workflows, refresh CHANGELOG.md through pm-changelog 2026.6.4, and update the Sentry release gate to treat handled one-error validation CommandErrors as expected CLI errors.

Verification: pm test pm-vnjh --run --progress passed 11/11 linked commands including coverage at 100%, typecheck, build, docs/static/security gates, pm-changelog external smoke, Sentry/telemetry gate, and telemetry stack health.
@unbraind

unbraind commented Jun 4, 2026

Copy link
Copy Markdown
Owner Author

@codex review
/gemini review
@gemini-code-assist review

Please review the package ecosystem smoke harness, public SDK testing helper, pm-changelog update, and Sentry gate handled-error classification in this PR.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @unbraind, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jun 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@unbraind, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 17 minutes and 43 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: b82b6df5-7eab-4c7e-b62d-6fe05d463f80

📥 Commits

Reviewing files that changed from the base of the PR and between 991a114 and ddb4a3a.

📒 Files selected for processing (6)
  • .agents/pm/extensions/.managed-extensions.json
  • .agents/pm/features/pm-vnjh.toon
  • .agents/pm/history/pm-vnjh.jsonl
  • scripts/smoke-external-packages.mjs
  • src/sdk/testing.ts
  • tests/unit/sdk-index.spec.ts
📝 Walkthrough

Walkthrough

This PR delivers SDK in-memory extension testing utilities, a comprehensive external npm package ecosystem smoke harness for verification after build, a pm-changelog extension release (2026.6.4) with new opt-in CLI flags, release gate refinements for Sentry validation, and corresponding documentation updates.

Changes

SDK testing utilities and ecosystem smoke infrastructure

Layer / File(s) Summary
SDK in-memory extension activation testing
src/sdk/testing.ts, src/sdk/index.ts, tests/unit/sdk-index.spec.ts, docs/SDK.md, docs/EXTENSIONS.md
New activateExtensionForTest function enables activating in-memory extension modules with optional manifest, governance policy, and capability configuration; SDK barrel re-exports the function and type; comprehensive unit tests verify manifest handling, default naming, non-module handling, and capability guardrail enforcement; API documentation explains the testing pattern and usage example; package authoring guidance clarifies when to use in-memory testing vs. integration checks.
External package ecosystem smoke test harness
scripts/smoke-external-packages.mjs, package.json, docs/EXTENSIONS.md, docs/TESTING.md
New smoke harness script discovers npm packages via search (filtered by pm-package keyword) or explicit list, creates isolated temporary projects with sandboxed PM environment paths, sequences CLI operations (init, install, package doctor, contracts), detects and reports activation/blocking failures, extracts available runtime actions, and outputs structured JSON results; npm script smoke:external-packages wires the harness into the build pipeline; ecosystem maintenance and testing documentation explain discovery, isolation, verification, and debugging modes.
Release gate Sentry validation handling
scripts/release/sentry-telemetry-gate.mjs, tests/integration/release-automation-contract.spec.ts
Sentry telemetry gate now recognizes "validation failed: 1 structural error(s)" as an expected/handled CLI issue pattern; integration test asserts the gate source includes the validation failure message for contract coverage.
pm-changelog extension release (2026.6.4)
.agents/pm/extensions/.managed-extensions.json, .agents/pm/extensions/pm-changelog/manifest.json, .agents/pm/extensions/pm-changelog/package.json, .agents/pm/extensions/pm-changelog/CHANGELOG.md, .agents/pm/extensions/pm-changelog/README.md, .agents/pm/extensions/pm-changelog/docs/usage.md
Extension version bumped from 2026.6.3 to 2026.6.4 in manifest and package metadata; changelog documents new opt-in flags (--breaking-changes, --suggest-semver, --body-preview <n>, --emoji-prefix) and label-deduplication fix; README and usage docs provide examples and detailed descriptions of each flag's behavior and output formatting.
Work tracking and project management artifacts
.agents/pm/epics/pm-u9d0.toon, .agents/pm/epics/pm-ugqx.toon, .agents/pm/features/pm-vnjh.toon, .agents/pm/history/*.jsonl, CHANGELOG.md
Project management records document work progression: epic updates with implementation evidence and test results; feature closure with resolution fields, expected vs. actual outcomes, artifact references, and test execution logs; JSONL history entries track claims, artifact additions, test runs, and release steps; top-level CHANGELOG entry adds reusable smoke harness feature.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • unbraind/pm-cli#99: Both PRs extend KNOWN_EXPECTED_HANDLED_CLI_ISSUE_PATTERNS in the Sentry release-gating logic with additional expected validation message patterns.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and concisely summarizes the two main changes: adding a package ecosystem smoke harness and exposing an SDK test helper, which are the primary objectives of this changeset.
Description check ✅ Passed The pull request description is directly related to the changeset, detailing the smoke harness implementation, SDK test helper exposure, documentation updates, verification results, and PM tracking.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a reusable external npm package smoke harness to test package compatibility in isolated environments, along with a public activateExtensionForTest SDK helper to facilitate in-memory extension testing for package authors. It also adds several opt-in features to the pm-changelog extension (such as breaking changes, semver suggestions, body previews, and emoji prefixes) and updates the Sentry release gate configuration. The review feedback highlights several robustness improvements for the new smoke harness script, including resolving the repository root relative to the script location, capturing execution errors from spawnSync, and safely catching potential cleanup failures in rmSync. Additionally, it is recommended to defensively check that capabilities are arrays before spreading them in the SDK testing helper.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +3 to +11
import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";

const repoRoot = process.cwd();
const cliPath = path.join(repoRoot, "dist", "cli.js");
const defaultQuery = "keywords:pm-package";
const ecosystemMarkers = ["pm-package", "pm-cli", "pm-extension", "pm-cli-extension"];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using process.cwd() to resolve the repository root can cause the script to fail if it is executed from a subdirectory (e.g., from within the scripts/ directory itself). It is more robust to resolve the repository root relative to the script's own location using import.meta.url.

import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..");
const cliPath = path.join(repoRoot, "dist", "cli.js");
const defaultQuery = "keywords:pm-package";
const ecosystemMarkers = ["pm-package", "pm-cli", "pm-extension", "pm-cli-extension"];

Comment on lines +92 to +110
function runCommand(command, args, options = {}) {
const startedAt = Date.now();
const result = spawnSync(command, args, {
cwd: options.cwd ?? repoRoot,
encoding: "utf8",
env: options.env ?? process.env,
maxBuffer: 20 * 1024 * 1024,
timeout: options.timeoutMs,
});
const tookMs = Date.now() - startedAt;
return {
code: result.status ?? 1,
signal: result.signal ?? null,
stdout: result.stdout ?? "",
stderr: result.stderr ?? "",
took_ms: tookMs,
command: [command, ...args].join(" "),
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If spawnSync fails to execute the command (for example, if npm is not installed or not found in the system's PATH), it returns an object with result.error (e.g., ENOENT), and result.status is null. Currently, runCommand ignores result.error and returns an empty stderr, which makes debugging extremely difficult as the error message will be completely empty. We should capture result.error and populate stderr with its message.

Suggested change
function runCommand(command, args, options = {}) {
const startedAt = Date.now();
const result = spawnSync(command, args, {
cwd: options.cwd ?? repoRoot,
encoding: "utf8",
env: options.env ?? process.env,
maxBuffer: 20 * 1024 * 1024,
timeout: options.timeoutMs,
});
const tookMs = Date.now() - startedAt;
return {
code: result.status ?? 1,
signal: result.signal ?? null,
stdout: result.stdout ?? "",
stderr: result.stderr ?? "",
took_ms: tookMs,
command: [command, ...args].join(" "),
};
}
function runCommand(command, args, options = {}) {
const startedAt = Date.now();
const result = spawnSync(command, args, {
cwd: options.cwd ?? repoRoot,
encoding: "utf8",
env: options.env ?? process.env,
maxBuffer: 20 * 1024 * 1024,
timeout: options.timeoutMs,
});
const tookMs = Date.now() - startedAt;
return {
code: result.status ?? (result.error ? 1 : 0),
signal: result.signal ?? null,
stdout: result.stdout ?? "",
stderr: result.stderr || (result.error ? result.error.message : ""),
took_ms: tookMs,
command: [command, ...args].join(" "),
};
}

Comment on lines +244 to +248
} finally {
if (!options.keepTemp) {
rmSync(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If rmSync fails (for example, due to temporary file locks on Windows or permission issues), it will throw an unhandled exception in the finally block. This will crash the entire script and prevent subsequent packages from being smoke-tested. Wrapping rmSync in a try...catch block ensures that a cleanup failure for one package does not abort the entire test run.

Suggested change
} finally {
if (!options.keepTemp) {
rmSync(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
}
}
} finally {
if (!options.keepTemp) {
try {
rmSync(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
} catch (cleanupError) {
console.error(`Failed to clean up temp directory ${tempRoot}:`, cleanupError);
}
}
}

Comment thread src/sdk/testing.ts Outdated
options.name ??
(typeof manifest.name === "string" && manifest.name.trim().length > 0 ? manifest.name.trim() : "test-extension");
const layer = options.layer ?? "project";
const capabilities = [...(options.capabilities ?? (manifest.capabilities as ExtensionCapability[] | undefined) ?? [])];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If manifest.capabilities is not an array (e.g., if it is malformed or not provided as an array in a custom test manifest), spreading it directly with ... will throw a runtime TypeError: is not iterable. We should defensively check if options.capabilities or manifest.capabilities are arrays before spreading them to prevent runtime crashes.

  const capabilities = [
    ...(Array.isArray(options.capabilities)
      ? options.capabilities
      : Array.isArray(manifest.capabilities)
      ? (manifest.capabilities as ExtensionCapability[])
      : []),
  ];

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a reusable external npm package ecosystem smoke harness to discover and test external packages in isolated environments, and adds activateExtensionForTest to the public SDK testing surface for in-memory extension activation testing. It also updates Sentry telemetry gates, documentation, and adds corresponding integration and unit tests. Feedback on these changes highlights a potential resource leak and missing directory creation in the smoke harness script, as well as a potential TypeError in the SDK testing helper if manifest.capabilities is not an array when spread.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +178 to +248
const tempRoot = mkdtempSync(path.join(tmpdir(), "pm-external-package-smoke-"));
const projectRoot = path.join(tempRoot, "project");
const pmPath = path.join(projectRoot, ".agents", "pm");
const globalPath = path.join(tempRoot, "global");
mkdirSync(projectRoot, { recursive: true });
writeFileSync(path.join(projectRoot, "README.md"), "# External package smoke project\n", "utf8");

const env = {
...process.env,
PM_PATH: pmPath,
PM_GLOBAL_PATH: globalPath,
PM_AUTHOR: "external-package-smoke",
PM_TELEMETRY_SOURCE_CONTEXT: process.env.PM_TELEMETRY_SOURCE_CONTEXT || "external-package-smoke",
};
const commands = [];
const startedAt = Date.now();

function record(label, args) {
const result = runPm(label, args, env, {
cwd: projectRoot,
timeoutMs: options.timeoutMs,
});
commands.push(result.entry);
if (result.entry.code !== 0) {
throw new Error(result.entry.error || `${label} failed`);
}
return result.payload;
}

try {
record("init", ["init", "--defaults", "--author", "external-package-smoke"]);
const install = record("install", ["install", `npm:${packageName}`, "--project"]);
const doctor = record("package doctor", ["package", "doctor", "--project", "--detail", "deep", "--trace"]);
const contracts = record("runtime contracts", ["contracts", "--runtime-only", "--availability-only"]);

const summary = doctor?.details?.summary ?? {};
const activationFailures = summary.activation_failure_count ?? 0;
const blockingFailures = summary.blocking_failure_count ?? 0;
if (activationFailures > 0 || blockingFailures > 0) {
throw new Error(`package doctor reported activation=${activationFailures} blocking=${blockingFailures}`);
}

const availableActions = Array.isArray(contracts?.action_availability)
? contracts.action_availability.filter((entry) => entry?.available === true).map((entry) => entry.action)
: [];
return {
package: packageName,
ok: true,
took_ms: Date.now() - startedAt,
temp_root: options.keepTemp ? tempRoot : undefined,
installed_count: install?.details?.installed_count ?? null,
activation_failure_count: activationFailures,
blocking_failure_count: blockingFailures,
warning_codes: doctor?.details?.triage?.warning_codes ?? [],
available_runtime_actions: availableActions,
commands,
};
} catch (error) {
return {
package: packageName,
ok: false,
took_ms: Date.now() - startedAt,
temp_root: options.keepTemp ? tempRoot : undefined,
error: error instanceof Error ? error.message : String(error),
commands,
};
} finally {
if (!options.keepTemp) {
rmSync(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a potential resource leak if any setup steps (such as mkdirSync or writeFileSync) fail before entering the try block. The temporary directory created by mkdtempSync will not be cleaned up because the finally block is only executed if the try block is entered.

Additionally, the globalPath directory is defined but never created on disk, which could cause issues if the pm CLI expects it to exist.

To fix both issues, declare commands and startedAt outside, start the try block immediately after creating tempRoot, and ensure both projectRoot and globalPath are created recursively.

  const tempRoot = mkdtempSync(path.join(tmpdir(), "pm-external-package-smoke-"));
  const commands = [];
  const startedAt = Date.now();
  try {
    const projectRoot = path.join(tempRoot, "project");
    const pmPath = path.join(projectRoot, ".agents", "pm");
    const globalPath = path.join(tempRoot, "global");
    mkdirSync(projectRoot, { recursive: true });
    mkdirSync(globalPath, { recursive: true });
    writeFileSync(path.join(projectRoot, "README.md"), "# External package smoke project\n", "utf8");

    const env = {
      ...process.env,
      PM_PATH: pmPath,
      PM_GLOBAL_PATH: globalPath,
      PM_AUTHOR: "external-package-smoke",
      PM_TELEMETRY_SOURCE_CONTEXT: process.env.PM_TELEMETRY_SOURCE_CONTEXT || "external-package-smoke",
    };

    function record(label, args) {
      const result = runPm(label, args, env, {
        cwd: projectRoot,
        timeoutMs: options.timeoutMs,
      });
      commands.push(result.entry);
      if (result.entry.code !== 0) {
        throw new Error(result.entry.error || label + " failed");
      }
      return result.payload;
    }

    record("init", ["init", "--defaults", "--author", "external-package-smoke"]);
    const install = record("install", ["install", "npm:" + packageName, "--project"]);
    const doctor = record("package doctor", ["package", "doctor", "--project", "--detail", "deep", "--trace"]);
    const contracts = record("runtime contracts", ["contracts", "--runtime-only", "--availability-only"]);

    const summary = doctor?.details?.summary ?? {};
    const activationFailures = summary.activation_failure_count ?? 0;
    const blockingFailures = summary.blocking_failure_count ?? 0;
    if (activationFailures > 0 || blockingFailures > 0) {
      throw new Error("package doctor reported activation=" + activationFailures + " blocking=" + blockingFailures);
    }

    const availableActions = Array.isArray(contracts?.action_availability)
      ? contracts.action_availability.filter((entry) => entry?.available === true).map((entry) => entry.action)
      : [];
    return {
      package: packageName,
      ok: true,
      took_ms: Date.now() - startedAt,
      temp_root: options.keepTemp ? tempRoot : undefined,
      installed_count: install?.details?.installed_count ?? null,
      activation_failure_count: activationFailures,
      blocking_failure_count: blockingFailures,
      warning_codes: doctor?.details?.triage?.warning_codes ?? [],
      available_runtime_actions: availableActions,
      commands,
    };
  } catch (error) {
    return {
      package: packageName,
      ok: false,
      took_ms: Date.now() - startedAt,
      temp_root: options.keepTemp ? tempRoot : undefined,
      error: error instanceof Error ? error.message : String(error),
      commands,
    };
  } finally {
    if (!options.keepTemp) {
      rmSync(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
    }
  }

Comment thread src/sdk/testing.ts Outdated
options.name ??
(typeof manifest.name === "string" && manifest.name.trim().length > 0 ? manifest.name.trim() : "test-extension");
const layer = options.layer ?? "project";
const capabilities = [...(options.capabilities ?? (manifest.capabilities as ExtensionCapability[] | undefined) ?? [])];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If manifest.capabilities is not an array (for example, if a user accidentally passes a string like "commands"), spreading it directly using the spread operator will either throw a TypeError or spread the string into individual characters. To ensure robust defensive programming, check if manifest.capabilities is an array before spreading.

Suggested change
const capabilities = [...(options.capabilities ?? (manifest.capabilities as ExtensionCapability[] | undefined) ?? [])];
const manifestCapabilities = Array.isArray(manifest.capabilities) ? manifest.capabilities : [];
const capabilities = [...(options.capabilities ?? manifestCapabilities)];

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/smoke-external-packages.mjs`:
- Around line 220-222: The code currently builds availableActions from
contracts.action_availability by checking entry?.available; update that to use
the runtime contract’s invocable flag instead: filter entries by
entry?.invocable === true when computing availableActions (the
Array.isArray(contracts?.action_availability) branch that maps to entry.action
should remain the same), so availableActions reflects
action_availability[].invocable as described in the SDK/runtime docs.

In `@src/sdk/testing.ts`:
- Line 140: The spread of manifest.capabilities can throw if
manifest.capabilities is not an array; update the logic around the const
capabilities = ... line to validate manifest.capabilities with Array.isArray
before spreading (use options.capabilities if provided, else if
Array.isArray(manifest.capabilities) use that, otherwise fall back to an empty
array) so malformed manifests like { capabilities: "commands" } or numbers won't
crash; reference the existing symbols options.capabilities and
manifest.capabilities and ensure the resulting capabilities is always an array
of ExtensionCapability.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 583df0c8-f3cb-427a-bf19-abf2d5b47ef5

📥 Commits

Reviewing files that changed from the base of the PR and between 21f2bab and 991a114.

⛔ Files ignored due to path filters (16)
  • .agents/pm/extensions/pm-changelog/dist/cli.js is excluded by !**/dist/**
  • .agents/pm/extensions/pm-changelog/dist/cli.js.map is excluded by !**/dist/**, !**/*.map
  • .agents/pm/extensions/pm-changelog/dist/extension.d.ts.map is excluded by !**/dist/**, !**/*.map
  • .agents/pm/extensions/pm-changelog/dist/extension.js is excluded by !**/dist/**
  • .agents/pm/extensions/pm-changelog/dist/extension.js.map is excluded by !**/dist/**, !**/*.map
  • .agents/pm/extensions/pm-changelog/dist/generator.d.ts is excluded by !**/dist/**
  • .agents/pm/extensions/pm-changelog/dist/generator.d.ts.map is excluded by !**/dist/**, !**/*.map
  • .agents/pm/extensions/pm-changelog/dist/generator.js is excluded by !**/dist/**
  • .agents/pm/extensions/pm-changelog/dist/generator.js.map is excluded by !**/dist/**, !**/*.map
  • .agents/pm/extensions/pm-changelog/dist/index.d.ts is excluded by !**/dist/**
  • .agents/pm/extensions/pm-changelog/dist/index.d.ts.map is excluded by !**/dist/**, !**/*.map
  • .agents/pm/extensions/pm-changelog/dist/index.js is excluded by !**/dist/**
  • .agents/pm/extensions/pm-changelog/dist/index.js.map is excluded by !**/dist/**, !**/*.map
  • .agents/pm/extensions/pm-changelog/dist/types.d.ts is excluded by !**/dist/**
  • .agents/pm/extensions/pm-changelog/dist/types.d.ts.map is excluded by !**/dist/**, !**/*.map
  • .agents/pm/extensions/pm-changelog/dist/types.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (23)
  • .agents/pm/epics/pm-u9d0.toon
  • .agents/pm/epics/pm-ugqx.toon
  • .agents/pm/extensions/.managed-extensions.json
  • .agents/pm/extensions/pm-changelog/CHANGELOG.md
  • .agents/pm/extensions/pm-changelog/README.md
  • .agents/pm/extensions/pm-changelog/docs/usage.md
  • .agents/pm/extensions/pm-changelog/manifest.json
  • .agents/pm/extensions/pm-changelog/package.json
  • .agents/pm/features/pm-vnjh.toon
  • .agents/pm/history/pm-u9d0.jsonl
  • .agents/pm/history/pm-ugqx.jsonl
  • .agents/pm/history/pm-vnjh.jsonl
  • CHANGELOG.md
  • docs/EXTENSIONS.md
  • docs/SDK.md
  • docs/TESTING.md
  • package.json
  • scripts/release/sentry-telemetry-gate.mjs
  • scripts/smoke-external-packages.mjs
  • src/sdk/index.ts
  • src/sdk/testing.ts
  • tests/integration/release-automation-contract.spec.ts
  • tests/unit/sdk-index.spec.ts

Comment thread scripts/smoke-external-packages.mjs Resolved
Comment thread src/sdk/testing.ts Resolved

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 991a114ab9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@@ -177,6 +193,7 @@ export function buildChangelogDocument(options) {
section_by: sectionBy,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Base semver suggestion on visible release sections

When --changelog-json is combined with release-window output and visibility flags such as --all-release-tags --limit 1 --suggest-semver or --since-version, the JSON releases array is narrowed via visibleSections, but suggestSemver(options) recomputes over filterItems(options) and ignores those section limits. This can report a major/minor/patch bump because of older hidden releases that are not present in the emitted changelog document; compute the suggestion from the same visibleSections items instead.

Useful? React with 👍 / 👎.

Comment thread src/sdk/testing.ts Outdated
Comment on lines +117 to +119
const manifest = (module as TestExtensionModule).manifest;
if (manifest && typeof manifest === "object") {
return manifest;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read manifests from default-exported modules

When package tests pass the ESM namespace returned by await import(...) for a default-exported extension, activation later finds default.activate, but this helper only reads module.manifest. A default export shaped like export default defineExtension({ manifest: { capabilities: ["commands"] }, activate(...) { ... } }) therefore falls back to test-extension with an empty capabilities array, causing real command/schema registrations to fail unless the test duplicates metadata in options; check module.default.manifest when the top-level manifest is absent.

Useful? React with 👍 / 👎.

const limit = options.bodyPreview && options.bodyPreview > 0 ? options.bodyPreview : 0;
if (limit === 0)
return "";
const body = typeof item.body === "string" ? toSingleLine(item.body) : "";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load item bodies for body previews

With the normal npx pm-changelog --body-preview path and the pm changelog generate --body-preview extension command, items are loaded via pm list-all --json / listAllFrontMatter, which do not include the document body unless the body-specific list path is requested. As a result item.body is usually absent here and the new flag silently emits no previews for real pm items; load items with bodies when bodyPreview is set, or document that this only works for custom stdin JSON containing body.

Useful? React with 👍 / 👎.

]
.join(" ")
.toLowerCase();
return haystack.includes("breaking");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid treating non-breaking text as breaking

For items whose type, tag, or title says something like non-breaking fix, this substring check still marks the item as breaking and makes --suggest-semver recommend a major bump. That is a common release-note phrase for explicitly safe changes, so the breaking detector should require a real breaking token/signal rather than any occurrence inside a negated word.

Useful? React with 👍 / 👎.

Harden the external package smoke harness with script-relative repo root resolution, spawn error reporting, global temp root creation, cleanup guards, and invocable action reporting.

Make activateExtensionForTest tolerate malformed manifest capabilities and default-exported manifests, with regression coverage.

Record review-response evidence on pm-vnjh and route package-owned pm-changelog follow-ups upstream as issues #26, #27, and #28.
@unbraind

unbraind commented Jun 4, 2026

Copy link
Copy Markdown
Owner Author

@codex review
/gemini review
@gemini-code-assist review

Review-response commit pushed (1bfacca). Please re-review the hardened smoke harness and SDK testing helper changes.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a reusable external npm package ecosystem smoke harness and adds activateExtensionForTest to the public SDK testing surface to enable in-memory extension testing for package authors. It also updates the Sentry telemetry gate to handle one-error validation issues and upgrades the pm-changelog extension with several opt-in features. Feedback on the changes suggests making the readTestExtensionManifest helper more robust by allowing it to fall back to reading metadata properties directly from standard extension modules when the nested manifest property is absent.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/sdk/testing.ts
Comment on lines +116 to +128
function readTestExtensionManifest(module: unknown): Partial<ExtensionManifest> {
if (module && typeof module === "object") {
const manifest = (module as TestExtensionModule).manifest;
if (manifest && typeof manifest === "object") {
return manifest;
}
const defaultManifest = (module as TestExtensionModule).default?.manifest;
if (defaultManifest && typeof defaultManifest === "object") {
return defaultManifest;
}
}
return {};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The readTestExtensionManifest helper currently only extracts metadata if it is nested under a manifest property (either directly on the module or on its default export). However, standard extensions created via defineExtension typically export the extension object directly (or as a default export) with metadata properties like name, version, and capabilities directly on it.

To make this helper more robust and intuitive for package authors, we should fall back to reading these metadata properties directly from the module or its default export when the manifest property is absent.

function readTestExtensionManifest(module: unknown): Partial<ExtensionManifest> {
  if (module && typeof module === "object") {
    const mod = module as Record<string, unknown>;
    if (mod.manifest && typeof mod.manifest === "object") {
      return mod.manifest as Partial<ExtensionManifest>;
    }
    const defaultExport = mod.default as Record<string, unknown> | undefined;
    if (defaultExport && typeof defaultExport === "object") {
      if (defaultExport.manifest && typeof defaultExport.manifest === "object") {
        return defaultExport.manifest as Partial<ExtensionManifest>;
      }
      if ("name" in defaultExport || "capabilities" in defaultExport) {
        return defaultExport as Partial<ExtensionManifest>;
      }
    }
    if ("name" in mod || "capabilities" in mod) {
      return mod as Partial<ExtensionManifest>;
    }
  }
  return {};
}

@unbraind

unbraind commented Jun 4, 2026

Copy link
Copy Markdown
Owner Author

@codex review
/gemini review
@gemini-code-assist review

Second review-response commit pushed (a75510d). Please re-review the SDK testing helper metadata fallback and added regression coverage.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a reusable external npm package ecosystem smoke harness script and exposes a new public SDK testing helper, activateExtensionForTest, for in-memory extension activation testing, along with updated documentation. Additionally, the pm-changelog extension is updated to version 2026.6.4 with several new opt-in features. Review feedback suggests improving the robustness of the smoke harness script by handling JSON parsing errors gracefully, parsing comma-separated keywords in npm search results, and filtering out invalid action entries.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread scripts/smoke-external-packages.mjs Outdated
Comment on lines +171 to +176
const entry = { label, code: result.code, took_ms: result.took_ms };
if (result.code !== 0) {
entry.error = summarizeFailure(result);
return { entry, payload: null };
}
return { entry, payload: parseJsonOutput(result, label) };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If parseJsonOutput throws an error (e.g., due to invalid JSON or empty stdout), runPm will throw an unhandled exception. This bypasses the return statement in runPm, meaning that record never receives the result and cannot push the failed command's entry into the commands array. As a result, the final failure report will be missing the command that caused the JSON parsing failure.

We should wrap the JSON parsing in a try-catch block inside runPm to gracefully capture the failure as a command entry with a non-zero exit code.

Suggested change
const entry = { label, code: result.code, took_ms: result.took_ms };
if (result.code !== 0) {
entry.error = summarizeFailure(result);
return { entry, payload: null };
}
return { entry, payload: parseJsonOutput(result, label) };
const entry = { label, code: result.code, took_ms: result.took_ms };
if (result.code !== 0) {
entry.error = summarizeFailure(result);
return { entry, payload: null };
}
try {
return { entry, payload: JSON.parse(result.stdout) };
} catch (error) {
entry.code = 1;
entry.error = `Failed to parse JSON output: ${error instanceof Error ? error.message : String(error)}`;
return { entry, payload: null };
}

Comment thread scripts/smoke-external-packages.mjs Outdated
if (!entry || typeof entry.name !== "string" || entry.name.length === 0) {
return false;
}
const keywords = Array.isArray(entry.keywords) ? entry.keywords : [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In npm search --json, the keywords field can sometimes be returned as a comma-separated string or undefined rather than a pre-parsed array of strings. Checking only Array.isArray(entry.keywords) might cause the search to miss relevant keywords when they are formatted as a string.

We can improve robustness by parsing comma-separated strings into an array of keywords.

Suggested change
const keywords = Array.isArray(entry.keywords) ? entry.keywords : [];
const keywords = Array.isArray(entry.keywords)
? entry.keywords
: typeof entry.keywords === "string"
? entry.keywords.split(/[\s,]+/)
: [];

Comment on lines +224 to +226
const availableActions = Array.isArray(contracts?.action_availability)
? contracts.action_availability.filter((entry) => entry?.invocable === true).map((entry) => entry.action)
: [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When mapping contracts.action_availability, if entry.action is undefined or not a string, it will map to undefined in the resulting array. It is safer to filter out any entries that do not have a valid string action property to avoid pushing undefined values into available_runtime_actions.

    const availableActions = Array.isArray(contracts?.action_availability)
      ? contracts.action_availability
          .filter((entry) => entry?.invocable === true && typeof entry?.action === "string")
          .map((entry) => entry.action)
      : [];

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a75510d2ee

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/sdk/testing.ts
Comment on lines +145 to +147
if (Array.isArray(manifest.capabilities)) {
return manifest.capabilities.filter((capability): capability is ExtensionCapability => typeof capability === "string");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize legacy aliases in test capabilities

When an in-memory test manifest uses supported legacy capability aliases such as validation or migration, this helper passes the raw strings through here, while real filesystem-loaded manifests are normalized via normalizeManifestCapabilities before activation. Because assertExtensionCapability only recognizes canonical capability names, a package-author test using a runtime-valid alias for schema will fail with a missing-capability activation error even though the same manifest passes the real loader/doctor path.

Useful? React with 👍 / 👎.

section_by: sectionBy,
item_count: visibleSections.reduce((sum, section) => sum + section.items.length, 0),
releases,
suggested_semver: options.suggestSemver ? suggestSemver(options) : undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope semver suggestions to visible releases

When --changelog-json --suggest-semver is combined with release-window filters such as --all-release-tags --limit 1 or --since-version, the JSON document's releases and item_count are computed from visibleSections, but this new field recomputes from the unbounded options. That makes the suggested bump count hidden older releases (for example, a breaking item outside the requested limit still reports major for a document containing only non-breaking changes), so downstream release automation can choose the wrong version bump.

Useful? React with 👍 / 👎.

]
.join(" ")
.toLowerCase();
return haystack.includes("breaking");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid treating non-breaking text as breaking

For the new --breaking-changes / --suggest-semver paths, checking for the substring breaking marks common titles or tags such as non-breaking API cleanup or not-breaking as a breaking change. In those cases the generated changelog gets an erroneous Breaking Changes entry and the semver suggestion is promoted to major even though the item explicitly says it is non-breaking.

Useful? React with 👍 / 👎.

@unbraind

unbraind commented Jun 4, 2026

Copy link
Copy Markdown
Owner Author

@codex review
/gemini review
@gemini-code-assist review

Third review-response commit pushed (ddb4a3a). Please re-review the smoke harness JSON parse failure recording, npm keyword normalization, and runtime action filtering changes.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a reusable external npm package ecosystem smoke harness to discover and test external packages in isolated environments, and adds a public SDK testing helper activateExtensionForTest to allow package authors to test extension activation in-memory. Additionally, the pm-changelog extension is updated to version 2026.6.4, introducing new opt-in features such as breaking changes sections, semver suggestions, body previews, and emoji prefixes. There are no review comments, so I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant