Files
vercel__vercel-plugin/tests/inject-claude-md.test.ts
Jathin Pranav Singaraju c9b1d8ff93 [marketplace] fix skill shadowed by command + discover-first routing (#107)
* feat(marketplace): discover-first commerce routing + provisioning guidance

Make the model reliably reach for the Marketplace when an app needs an
external service, instead of hardcoding a provider from memory.

- knowledge-update (always-injected at session start): add a discover-first
  directive — load the `marketplace` skill, then discover + provision a real
  integration BEFORE planning/writing code/asking.
- marketplace SKILL.md: broaden the description (commerce/payments/etc. as the
  catch-all for capabilities without a dedicated skill); restructure into
  discover -> install -> build with an anti-mock/anti-punt rule; add a
  Recommendations section keyed on the product-catalog test (commerce vs
  payments), with no hardcoded providers (discover names them).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(marketplace): remove command that shadowed the skill; tighten routing

The `marketplace` skill and `/marketplace` command shared a name, so the
command shadowed the skill — Skill(vercel:marketplace) loaded the command's
runbook instead, and the skill's routing guidance never reached the model.

Remove the command (marketplace is skill-only, like the other capabilities)
so the skill loads, and tighten routing: discover-first flow, a preferred-
provider table (commerce -> Shopify, payments -> Stripe), and a slimmer
knowledge-update that points at the skill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(release): patch-bump plugin manifests to 0.45.1

Backwards-compatible fix (marketplace skill routing); bump the version in
all three shipped manifests (.claude-plugin, .cursor-plugin, .plugin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:18:42 -07:00

120 lines
4.3 KiB
TypeScript

import { afterEach, beforeEach, describe, test, expect } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
const ROOT = resolve(import.meta.dirname, "..");
const HOOK_SCRIPT = join(ROOT, "hooks", "inject-claude-md.mjs");
let tempDir: string;
async function runHook(
payload: Record<string, unknown>,
env?: Record<string, string | undefined>,
): Promise<{ code: number; stdout: string; stderr: string }> {
const mergedEnv: Record<string, string> = { ...(process.env as Record<string, string>) };
for (const [key, value] of Object.entries(env || {})) {
if (value === undefined) {
delete mergedEnv[key];
} else {
mergedEnv[key] = value;
}
}
const proc = Bun.spawn(["node", HOOK_SCRIPT], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: mergedEnv,
});
proc.stdin.write(JSON.stringify(payload));
proc.stdin.end();
const code = await proc.exited;
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
return { code, stdout, stderr };
}
describe("inject-claude-md", () => {
beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), "inject-claude-md-"));
});
afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
});
test("injects thin session context instead of the full vercel ecosystem graph", async () => {
const { code, stdout } = await runHook({ session_id: "inject-thin-session" });
expect(code).toBe(0);
expect(stdout).toContain("Vercel Plugin Session Context");
expect(stdout).toContain("Vercel Knowledge Updates");
expect(stdout).not.toContain("Vercel Ecosystem — Relational Knowledge Graph");
});
test("appends greenfield guidance when VERCEL_PLUGIN_GREENFIELD=true", async () => {
const { code, stdout } = await runHook(
{ session_id: "inject-thin-greenfield" },
{ VERCEL_PLUGIN_GREENFIELD: "true" },
);
expect(code).toBe(0);
expect(stdout).toContain("Greenfield execution mode");
});
test("injects the always-on capability-routing contract", async () => {
// The load-bearing contract: every activated session gets the signal-gated
// routing table + the anti-hardcode rule, in the always-on layer.
const { code, stdout } = await runHook({ session_id: "inject-discover-first" });
expect(code).toBe(0);
expect(stdout).toContain("Integrations on Vercel");
// first-action contract: load the marketplace skill (which owns the discover-first workflow)
expect(stdout).toMatch(/first action/i);
expect(stdout).toMatch(/load the `?marketplace`? skill/i);
// anti-hardcode rule (the Stripe/provider-SDK failure mode)
expect(stdout).toMatch(/hardcode a provider SDK/i);
// skill-routing: the marketplace skill is the single always-on entry point
expect(stdout).toContain("`marketplace`");
// ambiguous stays loose — not over-strict
expect(stdout).toMatch(/ambiguous/i);
});
test("skips injection for non-empty non-vercel projects", async () => {
const projectDir = join(tempDir, "plain-project");
mkdirSync(projectDir);
writeFileSync(join(projectDir, "README.md"), "# Plain project");
const { code, stdout } = await runHook(
{ session_id: "inject-thin-skip" },
{ CLAUDE_PROJECT_ROOT: projectDir },
);
expect(code).toBe(0);
expect(stdout.trim()).toBe("");
});
test("still injects for empty directories", async () => {
const projectDir = join(tempDir, "greenfield-project");
mkdirSync(projectDir);
const { code, stdout } = await runHook(
{ session_id: "inject-thin-empty" },
{ CLAUDE_PROJECT_ROOT: projectDir },
);
expect(code).toBe(0);
expect(stdout).toContain("Vercel Plugin Session Context");
});
test("cursor payload returns flat JSON with thin additional context", async () => {
const { code, stdout } = await runHook({
conversation_id: "inject-thin-cursor",
cursor_version: "1.0.0",
workspace_roots: [ROOT],
});
expect(code).toBe(0);
const result = JSON.parse(stdout);
expect(result.additional_context).toContain("Vercel Plugin Session Context");
expect(result.additional_context).not.toContain("Vercel Ecosystem — Relational Knowledge Graph");
});
});