mirror of
https://github.com/vercel/vercel-plugin.git
synced 2026-09-14 15:39:47 +08:00
c9b1d8ff93
* 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>
120 lines
4.3 KiB
TypeScript
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");
|
|
});
|
|
});
|