mirror of
https://github.com/vercel/vercel-plugin.git
synced 2026-09-14 15:39:47 +08:00
8e24d0694a
Add a chainTo frontmatter field to skills that triggers follow-up skill injection when PostToolUse file contents match regex patterns. Add upgradeToSkill/upgradeWhy fields to validation rules so validate errors can recommend loading a specific skill. Register posttooluse-bash-chain.mjs in hooks.json. Add ChainToRule interface to patterns.mts and skill-map-frontmatter.mts, with parseChainToRules() parser. Add duplicate-key detection to the YAML parser. Reset dedup claim dir and session file on clear/compact events in session-start-seen-skills so skills re-inject into fresh context windows. Add chainTo rules to: agent-browser-verify, agent-browser, ai-elements, ai-gateway, ai-generation-persistence, ai-sdk, auth, bootstrap, chat-sdk, cms, cron-jobs, deployments-cicd, email, env-vars, geist, investigation-mode, json-render, marketplace, micro, ncc, next-forge, nextjs, observability, payments, react-best-practices, routing-middleware, runtime-cache, satori, shadcn, sign-in-with-vercel, swr, turbopack, turborepo, v0-dev, vercel-agent, vercel-api, vercel-cli, vercel-firewall, vercel-flags, vercel-functions, vercel-queues, vercel-sandbox, vercel-storage, verification, workflow. Add upgradeToSkill to ai-elements and ai-sdk validate rules. Expand ai-sdk validate messages with Run Skill() hints. Update nextjs, vercel-storage, runtime-cache, workflow, turborepo skill bodies. Add new skills: geistdocs (Geist design system docs), zzz-test-meta-name-mask (test fixture). Add skills/_chain-audit.md chain coverage audit doc. Delete .claude-plugin/marketplace.json, .claude-plugin/plugin.json (deprecated), skills/edge-runtime/SKILL.md (consolidated into vercel-functions). Add tests: posttooluse-chain.test.ts (4699 lines, chain injection e2e), ai-sdk-companion.test.ts (181 lines). Expand build-skill-map.test.ts (+335 lines), validate-rules.test.ts (+936 lines), session-start-seen-skills.test.ts (+74 lines), skill-map-frontmatter.test.ts (+50 lines), verification-skill.test.ts (+20 lines). Update build-manifest.ts to emit chainTo rules and upgradeToSkill fields. Rebuild generated/skill-manifest.json, generated/skill-catalog.md, generated/build-from-skills.manifest.json. Rebuild all compiled hooks/*.mjs. Update CLAUDE.md lexical prompt default to on. Update vercel.md ecosystem graph, docs, and cli-reference.
4700 lines
193 KiB
TypeScript
4700 lines
193 KiB
TypeScript
/**
|
|
* Tests for PostToolUse chain injection (chainTo rules).
|
|
*
|
|
* Covers:
|
|
* - chainTo match triggers skill injection in additionalContext
|
|
* - already-seen skill is NOT re-injected via chain
|
|
* - chain depth limit (single hop — no recursive chaining)
|
|
* - chainTo with no matches produces no additionalContext
|
|
* - multiple chainTo matches inject only highest-priority target (first match per target)
|
|
* - byte budget is respected for chained content
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach } from "bun:test";
|
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
|
|
const ROOT = resolve(import.meta.dirname, "..");
|
|
const HOOK_SCRIPT = join(ROOT, "hooks", "posttooluse-validate.mjs");
|
|
|
|
// Unique session ID per test run
|
|
let testSession: string;
|
|
|
|
beforeEach(() => {
|
|
testSession = `chain-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
});
|
|
|
|
/**
|
|
* Extract postValidation metadata from additionalContext.
|
|
*/
|
|
function extractPostValidation(hookSpecificOutput: any): any {
|
|
const ctx = hookSpecificOutput?.additionalContext || "";
|
|
const match = ctx.match(/<!-- postValidation: ({.*?}) -->/);
|
|
if (!match) return undefined;
|
|
try { return JSON.parse(match[1]); } catch { return undefined; }
|
|
}
|
|
|
|
async function runHook(
|
|
input: object,
|
|
extraEnv?: Record<string, string>,
|
|
): Promise<{ code: number; stdout: string; stderr: string; parsed: any; ctx: string }> {
|
|
const payload = JSON.stringify({ ...input, session_id: testSession });
|
|
const proc = Bun.spawn(["node", HOOK_SCRIPT], {
|
|
stdin: "pipe",
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: {
|
|
...process.env,
|
|
VERCEL_PLUGIN_VALIDATED_FILES: "",
|
|
VERCEL_PLUGIN_SEEN_SKILLS: "",
|
|
...extraEnv,
|
|
},
|
|
});
|
|
proc.stdin.write(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();
|
|
let parsed: any = {};
|
|
let ctx = "";
|
|
try {
|
|
parsed = JSON.parse(stdout);
|
|
ctx = parsed?.hookSpecificOutput?.additionalContext || "";
|
|
} catch {}
|
|
return { code, stdout, stderr, parsed, ctx };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Unit tests for runChainInjection (imported from compiled module)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("runChainInjection unit tests", () => {
|
|
let runChainInjection: typeof import("../hooks/src/posttooluse-validate.mts").runChainInjection;
|
|
let formatOutput: typeof import("../hooks/src/posttooluse-validate.mts").formatOutput;
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import("../hooks/posttooluse-validate.mjs");
|
|
runChainInjection = mod.runChainInjection;
|
|
formatOutput = mod.formatOutput;
|
|
});
|
|
|
|
test("chainTo match triggers skill injection", () => {
|
|
// Use a small target skill (micro ~3KB) to stay within 18KB budget
|
|
const chainMap = new Map([
|
|
["test-source", [
|
|
{
|
|
pattern: "SOME_PATTERN",
|
|
targetSkill: "micro",
|
|
message: "Loading micro guidance.",
|
|
},
|
|
]],
|
|
]);
|
|
|
|
const fileContent = `const x = SOME_PATTERN;\n`;
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["test-source"],
|
|
chainMap,
|
|
null, // no session (skip dedup file ops)
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].sourceSkill).toBe("test-source");
|
|
expect(result.injected[0].targetSkill).toBe("micro");
|
|
expect(result.injected[0].message).toBe("Loading micro guidance.");
|
|
expect(result.injected[0].content.length).toBeGreaterThan(0);
|
|
expect(result.totalBytes).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("already-seen skill is NOT re-injected via chain", () => {
|
|
const chainMap = new Map([
|
|
["test-source", [
|
|
{
|
|
pattern: "TRIGGER_PATTERN",
|
|
targetSkill: "micro",
|
|
},
|
|
]],
|
|
]);
|
|
|
|
const fileContent = `const x = TRIGGER_PATTERN;\n`;
|
|
const fakeEnv: any = {
|
|
VERCEL_PLUGIN_SEEN_SKILLS: "micro",
|
|
};
|
|
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["test-source"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
fakeEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(0);
|
|
expect(result.totalBytes).toBe(0);
|
|
});
|
|
|
|
test("chain depth is limited to 1 hop (no recursive chaining)", () => {
|
|
// Simulate: source-a chains to micro, which itself has chainTo rules.
|
|
// runChainInjection only processes the matchedSkills passed in — it does NOT
|
|
// recursively process chainTo rules of injected targets. This is the "single hop" guarantee.
|
|
const chainMap = new Map([
|
|
["source-a", [
|
|
{
|
|
pattern: "TRIGGER",
|
|
targetSkill: "micro", // ~3KB, fits in budget
|
|
},
|
|
]],
|
|
// micro also has a chain rule, but it should NOT fire
|
|
// because micro is not in matchedSkills
|
|
["micro", [
|
|
{
|
|
pattern: ".*",
|
|
targetSkill: "cron-jobs",
|
|
},
|
|
]],
|
|
]);
|
|
|
|
const fileContent = `const x = TRIGGER;\n`;
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["source-a"], // only source-a is matched — micro is NOT in matchedSkills
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
// Only micro should be injected (from source-a chain), NOT cron-jobs
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].targetSkill).toBe("micro");
|
|
// Confirm cron-jobs was NOT injected
|
|
expect(result.injected.every((i) => i.targetSkill !== "cron-jobs")).toBe(true);
|
|
});
|
|
|
|
test("chainTo with no matches produces no injections", () => {
|
|
const chainMap = new Map([
|
|
["test-source", [
|
|
{
|
|
pattern: "SOMETHING_THAT_WONT_MATCH",
|
|
targetSkill: "micro",
|
|
},
|
|
]],
|
|
]);
|
|
|
|
const fileContent = `import { generateText } from 'ai';\n`;
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["test-source"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(0);
|
|
expect(result.totalBytes).toBe(0);
|
|
});
|
|
|
|
test("multiple chainTo matches inject only one entry per target skill (first wins)", () => {
|
|
const chainMap = new Map([
|
|
["test-source", [
|
|
{
|
|
pattern: "FIRST_MATCH",
|
|
targetSkill: "micro",
|
|
message: "First chain rule",
|
|
},
|
|
{
|
|
pattern: "SECOND_MATCH",
|
|
targetSkill: "micro",
|
|
message: "Second chain rule — same target, should be deduped",
|
|
},
|
|
]],
|
|
]);
|
|
|
|
const fileContent = `const a = FIRST_MATCH;\nconst b = SECOND_MATCH;\n`;
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["test-source"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
// Only one injection for micro (first match wins)
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].targetSkill).toBe("micro");
|
|
expect(result.injected[0].message).toBe("First chain rule");
|
|
});
|
|
|
|
test("byte budget is respected for chained content", () => {
|
|
// micro (~3KB) + swr (~5KB) + cron-jobs (~2KB) = ~10KB — all fit within 18KB budget
|
|
// Raise cap to 10 so budget is the limiting factor, not the cap
|
|
const chainMap = new Map([
|
|
["source-a", [
|
|
{
|
|
pattern: "PATTERN_A",
|
|
targetSkill: "micro", // ~3KB
|
|
},
|
|
]],
|
|
["source-b", [
|
|
{
|
|
pattern: "PATTERN_B",
|
|
targetSkill: "swr", // ~5KB
|
|
},
|
|
]],
|
|
["source-c", [
|
|
{
|
|
pattern: "PATTERN_C",
|
|
targetSkill: "cron-jobs", // ~2KB
|
|
},
|
|
]],
|
|
]);
|
|
|
|
const fileContent = `PATTERN_A;\nPATTERN_B;\nPATTERN_C;\n`;
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "", VERCEL_PLUGIN_CHAIN_CAP: "10" };
|
|
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["source-a", "source-b", "source-c"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.totalBytes).toBeLessThanOrEqual(18_000);
|
|
expect(result.totalBytes).toBeGreaterThan(0);
|
|
// All three should fit within budget (cap raised to 10)
|
|
expect(result.injected.length).toBe(3);
|
|
});
|
|
|
|
test("chainTo with nonexistent target skill is skipped gracefully", () => {
|
|
const chainMap = new Map([
|
|
["test-source", [
|
|
{
|
|
pattern: "TRIGGER",
|
|
targetSkill: "nonexistent-skill-that-does-not-exist",
|
|
},
|
|
]],
|
|
]);
|
|
|
|
const fileContent = `const x = TRIGGER;\n`;
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["test-source"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(0);
|
|
});
|
|
|
|
test("chain injection is capped at VERCEL_PLUGIN_CHAIN_CAP (default 2)", () => {
|
|
// Three distinct targets, but cap is 2
|
|
const chainMap = new Map([
|
|
["source-a", [
|
|
{ pattern: "PAT_A", targetSkill: "micro" },
|
|
]],
|
|
["source-b", [
|
|
{ pattern: "PAT_B", targetSkill: "swr" },
|
|
]],
|
|
["source-c", [
|
|
{ pattern: "PAT_C", targetSkill: "cron-jobs" },
|
|
]],
|
|
]);
|
|
|
|
const fileContent = `PAT_A;\nPAT_B;\nPAT_C;\n`;
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["source-a", "source-b", "source-c"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
// Default cap is 2
|
|
expect(result.injected.length).toBe(2);
|
|
});
|
|
|
|
test("chain cap is configurable via VERCEL_PLUGIN_CHAIN_CAP env var", () => {
|
|
const chainMap = new Map([
|
|
["source-a", [
|
|
{ pattern: "PAT_A", targetSkill: "micro" },
|
|
]],
|
|
["source-b", [
|
|
{ pattern: "PAT_B", targetSkill: "swr" },
|
|
]],
|
|
["source-c", [
|
|
{ pattern: "PAT_C", targetSkill: "cron-jobs" },
|
|
]],
|
|
]);
|
|
|
|
const fileContent = `PAT_A;\nPAT_B;\nPAT_C;\n`;
|
|
|
|
// Cap set to 1
|
|
const envCap1: any = { VERCEL_PLUGIN_SEEN_SKILLS: "", VERCEL_PLUGIN_CHAIN_CAP: "1" };
|
|
const result1 = runChainInjection(
|
|
fileContent,
|
|
["source-a", "source-b", "source-c"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
envCap1,
|
|
);
|
|
expect(result1.injected.length).toBe(1);
|
|
|
|
// Cap set to 10 — all 3 should be injected
|
|
const envCap10: any = { VERCEL_PLUGIN_SEEN_SKILLS: "", VERCEL_PLUGIN_CHAIN_CAP: "10" };
|
|
const result10 = runChainInjection(
|
|
fileContent,
|
|
["source-a", "source-b", "source-c"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
envCap10,
|
|
);
|
|
expect(result10.injected.length).toBe(3);
|
|
});
|
|
|
|
test("skipIfFileContains skips chain rule when file matches the guard regex", () => {
|
|
const chainMap = new Map([
|
|
["test-source", [
|
|
{
|
|
pattern: "@vercel/postgres",
|
|
targetSkill: "micro",
|
|
skipIfFileContains: "@neondatabase/serverless",
|
|
},
|
|
]],
|
|
]);
|
|
|
|
// File already uses the replacement — chain should NOT fire
|
|
const fileContent = `import { neon } from '@neondatabase/serverless';\nimport { sql } from '@vercel/postgres';\n`;
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["test-source"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(0);
|
|
});
|
|
|
|
test("skipIfFileContains does NOT skip when guard regex does not match", () => {
|
|
const chainMap = new Map([
|
|
["test-source", [
|
|
{
|
|
pattern: "@vercel/postgres",
|
|
targetSkill: "micro",
|
|
skipIfFileContains: "@neondatabase/serverless",
|
|
},
|
|
]],
|
|
]);
|
|
|
|
// File uses deprecated import but NOT the replacement — chain should fire
|
|
const fileContent = `import { sql } from '@vercel/postgres';\n`;
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["test-source"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].targetSkill).toBe("micro");
|
|
});
|
|
|
|
test("loop prevention: A→B chain is blocked when B is already seen", () => {
|
|
// Simulate: skill A chains to B, but B is already seen
|
|
const chainMap = new Map([
|
|
["skill-a", [
|
|
{ pattern: "TRIGGER", targetSkill: "micro" },
|
|
]],
|
|
]);
|
|
|
|
const fileContent = `const x = TRIGGER;\n`;
|
|
const envWithSeen: any = { VERCEL_PLUGIN_SEEN_SKILLS: "micro" };
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["skill-a"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
envWithSeen,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(0);
|
|
});
|
|
|
|
test("loop prevention: bidirectional A↔B only injects once", () => {
|
|
// A chains to B, B chains to A — but A is already a matched skill (seen)
|
|
const chainMap = new Map([
|
|
["skill-a", [
|
|
{ pattern: "TRIGGER_B", targetSkill: "micro" },
|
|
]],
|
|
["micro", [
|
|
{ pattern: "TRIGGER_A", targetSkill: "skill-a" },
|
|
]],
|
|
]);
|
|
|
|
const fileContent = `TRIGGER_B;\nTRIGGER_A;\n`;
|
|
// skill-a is already seen (it was the matched skill that triggered this PostToolUse)
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "skill-a" };
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["skill-a"], // only skill-a is in matchedSkills — micro is NOT
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
// micro gets injected (skill-a chains to it)
|
|
// But micro's chain back to skill-a doesn't fire because micro isn't in matchedSkills
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].targetSkill).toBe("micro");
|
|
});
|
|
|
|
test("chainTo with invalid regex pattern is skipped gracefully", () => {
|
|
const chainMap = new Map([
|
|
["test-source", [
|
|
{
|
|
pattern: "[invalid(regex",
|
|
targetSkill: "micro",
|
|
},
|
|
]],
|
|
]);
|
|
|
|
const fileContent = `const x = TRIGGER;\n`;
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runChainInjection(
|
|
fileContent,
|
|
["test-source"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Real-world chain/validate scenario tests (use actual skill frontmatter)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("real-world chain and validate scenarios", () => {
|
|
let loadValidateRules: typeof import("../hooks/src/posttooluse-validate.mts").loadValidateRules;
|
|
let matchFileToSkills: typeof import("../hooks/src/posttooluse-validate.mts").matchFileToSkills;
|
|
let runValidation: typeof import("../hooks/src/posttooluse-validate.mts").runValidation;
|
|
let runChainInjection: typeof import("../hooks/src/posttooluse-validate.mts").runChainInjection;
|
|
|
|
let data: NonNullable<ReturnType<typeof loadValidateRules>>;
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import("../hooks/posttooluse-validate.mjs");
|
|
loadValidateRules = mod.loadValidateRules;
|
|
matchFileToSkills = mod.matchFileToSkills;
|
|
runValidation = mod.runValidation;
|
|
runChainInjection = mod.runChainInjection;
|
|
|
|
const loaded = loadValidateRules(ROOT);
|
|
if (!loaded) throw new Error("loadValidateRules returned null — no skills with validate/chainTo rules");
|
|
data = loaded;
|
|
});
|
|
|
|
test("workflow file with DurableAgent import (no ai-sdk) triggers ai-sdk chain", () => {
|
|
const filePath = "/project/workflows/review.ts";
|
|
const fileContent = [
|
|
`import { DurableAgent } from "@workflow/ai/agent";`,
|
|
`import { createWorkflow } from "workflow";`,
|
|
``,
|
|
`const wf = createWorkflow({ id: "review" });`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("workflow");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
expect(chainResult.injected.length).toBeGreaterThanOrEqual(1);
|
|
const aiSdkChain = chainResult.injected.find((i) => i.targetSkill === "ai-sdk");
|
|
expect(aiSdkChain).toBeDefined();
|
|
expect(aiSdkChain!.sourceSkill).toBe("workflow");
|
|
expect(aiSdkChain!.content.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("turbo.json with 'pipeline' key triggers turborepo validate error with upgradeToSkill", () => {
|
|
const filePath = "/project/turbo.json";
|
|
const fileContent = JSON.stringify({
|
|
"$schema": "https://turbo.build/schema.json",
|
|
"pipeline": {
|
|
"build": { "dependsOn": ["^build"] },
|
|
"lint": {},
|
|
},
|
|
}, null, 2);
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("turborepo");
|
|
|
|
const violations = runValidation(fileContent, matched, data.rulesMap);
|
|
const pipelineViolation = violations.find((v) => v.skill === "turborepo" && v.matchedText.includes("pipeline"));
|
|
expect(pipelineViolation).toBeDefined();
|
|
expect(pipelineViolation!.severity).toBe("error");
|
|
expect(pipelineViolation!.upgradeToSkill).toBe("turborepo");
|
|
expect(pipelineViolation!.message).toContain("tasks");
|
|
});
|
|
|
|
test("file with generateObject( triggers ai-sdk validate error", () => {
|
|
const filePath = "/project/app/api/extract/route.ts";
|
|
const fileContent = [
|
|
`import { generateObject } from 'ai';`,
|
|
`import { z } from 'zod';`,
|
|
``,
|
|
`const result = await generateObject({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` schema: z.object({ name: z.string() }),`,
|
|
` prompt: 'Extract the name',`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-sdk");
|
|
|
|
const violations = runValidation(fileContent, matched, data.rulesMap);
|
|
const genObjViolation = violations.find((v) => v.skill === "ai-sdk" && v.matchedText.includes("generateObject"));
|
|
expect(genObjViolation).toBeDefined();
|
|
expect(genObjViolation!.severity).toBe("error");
|
|
expect(genObjViolation!.message).toContain("Output.object");
|
|
});
|
|
|
|
test("file with maxSteps: triggers ai-sdk validate recommendation", () => {
|
|
const filePath = "/project/app/api/agent/route.ts";
|
|
const fileContent = [
|
|
`import { streamText } from 'ai';`,
|
|
``,
|
|
`const result = streamText({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` maxSteps: 5,`,
|
|
` prompt: 'Plan a trip',`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-sdk");
|
|
|
|
const violations = runValidation(fileContent, matched, data.rulesMap);
|
|
const maxStepsViolation = violations.find((v) => v.skill === "ai-sdk" && v.matchedText.includes("maxSteps"));
|
|
expect(maxStepsViolation).toBeDefined();
|
|
expect(maxStepsViolation!.severity).toBe("recommended");
|
|
expect(maxStepsViolation!.upgradeToSkill).toBe("ai-sdk");
|
|
expect(maxStepsViolation!.message).toContain("stepCountIs");
|
|
});
|
|
|
|
test("file with dall-e reference triggers ai-gateway upgrade", () => {
|
|
const filePath = "/project/app/api/image/route.ts";
|
|
const fileContent = [
|
|
`import { generateText } from 'ai';`,
|
|
``,
|
|
`const result = await generateText({`,
|
|
` model: 'openai/dall-e-3',`,
|
|
` prompt: 'A sunset over mountains',`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-sdk");
|
|
|
|
const violations = runValidation(fileContent, matched, data.rulesMap);
|
|
const dalleViolation = violations.find((v) => v.skill === "ai-sdk" && v.matchedText.includes("dall-e"));
|
|
expect(dalleViolation).toBeDefined();
|
|
expect(dalleViolation!.severity).toBe("recommended");
|
|
expect(dalleViolation!.upgradeToSkill).toBe("ai-gateway");
|
|
expect(dalleViolation!.message).toContain("gemini-3.1-flash-image-preview");
|
|
});
|
|
|
|
test("file with toDataStreamResponse triggers ai-sdk validate recommendation", () => {
|
|
const filePath = "/project/app/api/chat/route.ts";
|
|
const fileContent = [
|
|
`import { streamText } from 'ai';`,
|
|
``,
|
|
`export async function POST(req: Request) {`,
|
|
` const result = streamText({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` prompt: 'Hello!',`,
|
|
` });`,
|
|
` return result.toDataStreamResponse();`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-sdk");
|
|
|
|
const violations = runValidation(fileContent, matched, data.rulesMap);
|
|
const tdsViolation = violations.find((v) => v.skill === "ai-sdk" && v.matchedText.includes("toDataStreamResponse"));
|
|
expect(tdsViolation).toBeDefined();
|
|
expect(tdsViolation!.severity).toBe("recommended");
|
|
expect(tdsViolation!.message).toContain("toUIMessageStreamResponse");
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// New chainTo coverage: diverse cross-skill chain scenarios
|
|
// -------------------------------------------------------------------
|
|
|
|
test("vercel-storage file with @vercel/postgres import chains to nextjs", () => {
|
|
const filePath = "/project/lib/db.ts";
|
|
const fileContent = [
|
|
`import { sql } from '@vercel/postgres';`,
|
|
``,
|
|
`export async function getUsers() {`,
|
|
` const { rows } = await sql\`SELECT * FROM users\`;`,
|
|
` return rows;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("vercel-storage");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const nextjsChain = chainResult.injected.find((i) => i.targetSkill === "nextjs");
|
|
expect(nextjsChain).toBeDefined();
|
|
expect(nextjsChain!.sourceSkill).toBe("vercel-storage");
|
|
expect(nextjsChain!.message).toContain("@vercel/postgres");
|
|
expect(nextjsChain!.message).toContain("sunset");
|
|
});
|
|
|
|
test("shadcn file with react-markdown chains to ai-elements", () => {
|
|
const filePath = "/project/components/chat-display.tsx";
|
|
const fileContent = [
|
|
`import ReactMarkdown from 'react-markdown';`,
|
|
`import { cn } from '@/lib/utils';`,
|
|
``,
|
|
`export function ChatBubble({ text }: { text: string }) {`,
|
|
` return <ReactMarkdown>{text}</ReactMarkdown>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("shadcn");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const aiElementsChain = chainResult.injected.find((i) => i.targetSkill === "ai-elements");
|
|
expect(aiElementsChain).toBeDefined();
|
|
expect(aiElementsChain!.sourceSkill).toBe("shadcn");
|
|
expect(aiElementsChain!.message).toContain("markdown");
|
|
});
|
|
|
|
test("shadcn react-markdown chain is skipped when ai-elements already imported (skipIfFileContains)", () => {
|
|
const filePath = "/project/components/chat-display.tsx";
|
|
const fileContent = [
|
|
`import ReactMarkdown from 'react-markdown';`,
|
|
`import { MessageResponse } from '@/components/ai-elements/message';`,
|
|
`import { cn } from '@/lib/utils';`,
|
|
``,
|
|
`export function ChatBubble({ text }: { text: string }) {`,
|
|
` return <MessageResponse content={text} />;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("shadcn");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'ai-elements|MessageResponse|<Message\b' should suppress this chain
|
|
const aiElementsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "shadcn" && i.targetSkill === "ai-elements",
|
|
);
|
|
expect(aiElementsChain).toBeUndefined();
|
|
});
|
|
|
|
test("routing-middleware file with IP blocklist chains to vercel-firewall", () => {
|
|
const filePath = "/project/middleware.ts";
|
|
const fileContent = [
|
|
`import { NextRequest, NextResponse } from 'next/server';`,
|
|
``,
|
|
`const blockedIps = ['1.2.3.4', '5.6.7.8'];`,
|
|
``,
|
|
`export function middleware(req: NextRequest) {`,
|
|
` const ip = req.ip || '';`,
|
|
` if (blockedIps.includes(ip)) return NextResponse.json({}, { status: 403 });`,
|
|
` return NextResponse.next();`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("routing-middleware");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const firewallChain = chainResult.injected.find((i) => i.targetSkill === "vercel-firewall");
|
|
expect(firewallChain).toBeDefined();
|
|
expect(firewallChain!.sourceSkill).toBe("routing-middleware");
|
|
expect(firewallChain!.message).toContain("Firewall");
|
|
});
|
|
|
|
test("cron-jobs file with node-cron import chains to vercel-functions", () => {
|
|
const filePath = "/project/lib/scheduler.ts";
|
|
const fileContent = [
|
|
`import cron from 'node-cron';`,
|
|
``,
|
|
`cron.schedule('0 */6 * * *', async () => {`,
|
|
` await syncExternalData();`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("cron-jobs");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const functionsChain = chainResult.injected.find((i) => i.targetSkill === "vercel-functions");
|
|
expect(functionsChain).toBeDefined();
|
|
expect(functionsChain!.sourceSkill).toBe("cron-jobs");
|
|
expect(functionsChain!.message).toContain("cron");
|
|
});
|
|
|
|
test("react-best-practices file with axios import chains to swr", () => {
|
|
const filePath = "/project/components/UserList.tsx";
|
|
const fileContent = [
|
|
`import React, { useEffect, useState } from 'react';`,
|
|
`import axios from 'axios';`,
|
|
``,
|
|
`export function UserList() {`,
|
|
` const [users, setUsers] = useState([]);`,
|
|
` useEffect(() => {`,
|
|
` axios.get('/api/users').then(res => setUsers(res.data));`,
|
|
` }, []);`,
|
|
` return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("react-best-practices");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const swrChain = chainResult.injected.find((i) => i.targetSkill === "swr");
|
|
expect(swrChain).toBeDefined();
|
|
expect(swrChain!.sourceSkill).toBe("react-best-practices");
|
|
expect(swrChain!.message).toContain("axios");
|
|
});
|
|
|
|
test("payments file with setTimeout chains to workflow", () => {
|
|
const filePath = "/project/app/api/webhooks/stripe/route.ts";
|
|
const fileContent = [
|
|
`import Stripe from 'stripe';`,
|
|
``,
|
|
`export async function POST(req: Request) {`,
|
|
` const event = await parseWebhook(req);`,
|
|
` if (event.type === 'payment_intent.succeeded') {`,
|
|
` // Retry with backoff`,
|
|
` setTimeout(() => fulfillOrder(event.data.object), 5000);`,
|
|
` }`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("payments");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const workflowChain = chainResult.injected.find((i) => i.targetSkill === "workflow");
|
|
expect(workflowChain).toBeDefined();
|
|
expect(workflowChain!.sourceSkill).toBe("payments");
|
|
expect(workflowChain!.message).toContain("Workflow");
|
|
});
|
|
|
|
test("env-vars file with ANTHROPIC_API_KEY chains to ai-gateway", () => {
|
|
const filePath = "/project/.env.local";
|
|
const fileContent = [
|
|
`# AI provider keys`,
|
|
`ANTHROPIC_API_KEY=sk-ant-api...`,
|
|
`DATABASE_URL=postgres://...`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("env-vars");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const gatewayChain = chainResult.injected.find((i) => i.targetSkill === "ai-gateway");
|
|
expect(gatewayChain).toBeDefined();
|
|
expect(gatewayChain!.sourceSkill).toBe("env-vars");
|
|
expect(gatewayChain!.message).toContain("OIDC");
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Additional chainTo coverage: nextjs chains
|
|
// -------------------------------------------------------------------
|
|
|
|
test("nextjs file with middleware export chains to routing-middleware", () => {
|
|
const filePath = "/project/middleware.ts";
|
|
const fileContent = [
|
|
`import { NextResponse } from 'next/server';`,
|
|
``,
|
|
`export default function middleware(req) {`,
|
|
` return NextResponse.next();`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("nextjs");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const routingChain = chainResult.injected.find((i) => i.targetSkill === "routing-middleware");
|
|
expect(routingChain).toBeDefined();
|
|
expect(routingChain!.sourceSkill).toBe("nextjs");
|
|
expect(routingChain!.message).toContain("proxy");
|
|
});
|
|
|
|
test("nextjs file with @ai-sdk/openai chains to ai-gateway", () => {
|
|
const filePath = "/project/app/api/chat/route.ts";
|
|
const fileContent = [
|
|
`import { streamText } from 'ai';`,
|
|
`import { openai } from '@ai-sdk/openai';`,
|
|
``,
|
|
`export async function POST(req: Request) {`,
|
|
` const result = streamText({ model: openai('gpt-5.4'), prompt: 'Hello' });`,
|
|
` return result.toUIMessageStreamResponse();`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("nextjs");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const gatewayChain = chainResult.injected.find((i) => i.targetSkill === "ai-gateway");
|
|
expect(gatewayChain).toBeDefined();
|
|
expect(gatewayChain!.sourceSkill).toBe("nextjs");
|
|
expect(gatewayChain!.message).toContain("Gateway");
|
|
});
|
|
|
|
test("nextjs file with next-auth chains to auth", () => {
|
|
const filePath = "/project/app/api/auth/[...nextauth]/route.ts";
|
|
const fileContent = [
|
|
`import NextAuth from 'next-auth';`,
|
|
`import { authOptions } from '@/lib/auth';`,
|
|
``,
|
|
`const handler = NextAuth(authOptions);`,
|
|
`export { handler as GET, handler as POST };`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("nextjs");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const authChain = chainResult.injected.find((i) => i.targetSkill === "auth");
|
|
expect(authChain).toBeDefined();
|
|
expect(authChain!.sourceSkill).toBe("nextjs");
|
|
});
|
|
|
|
test("nextjs file with NextApiRequest chains to vercel-functions", () => {
|
|
const filePath = "/project/pages/api/users.ts";
|
|
const fileContent = [
|
|
`import type { NextApiRequest, NextApiResponse } from 'next';`,
|
|
``,
|
|
`export default function handler(req: NextApiRequest, res: NextApiResponse) {`,
|
|
` res.status(200).json({ users: [] });`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("nextjs");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const functionsChain = chainResult.injected.find((i) => i.targetSkill === "vercel-functions");
|
|
expect(functionsChain).toBeDefined();
|
|
expect(functionsChain!.sourceSkill).toBe("nextjs");
|
|
});
|
|
|
|
test("nextjs file with lru-cache chains to runtime-cache", () => {
|
|
const filePath = "/project/lib/cache.ts";
|
|
const fileContent = [
|
|
`import { LRUCache } from 'lru-cache';`,
|
|
``,
|
|
`const cache = new LRUCache({ max: 500, ttl: 60000 });`,
|
|
`export default cache;`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
// Should match nextjs via import or another skill
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const cacheChain = chainResult.injected.find((i) => i.targetSkill === "runtime-cache");
|
|
if (cacheChain) {
|
|
expect(cacheChain.message).toContain("cache");
|
|
}
|
|
});
|
|
|
|
test("nextjs file with JWT handling chains to auth (skipIfFileContains)", () => {
|
|
const filePath = "/project/lib/auth.ts";
|
|
const fileContent = [
|
|
`import jwt from 'jsonwebtoken';`,
|
|
``,
|
|
`export function verifyToken(token: string) {`,
|
|
` return jwt.verify(token, process.env.JWT_SECRET!);`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const authChain = chainResult.injected.find((i) => i.targetSkill === "auth");
|
|
if (authChain) {
|
|
expect(authChain.message).toContain("Auth");
|
|
}
|
|
});
|
|
|
|
test("nextjs JWT chain is skipped when Clerk is already imported (skipIfFileContains)", () => {
|
|
const filePath = "/project/lib/auth.ts";
|
|
const fileContent = [
|
|
`import { clerkMiddleware } from '@clerk/nextjs/server';`,
|
|
`import jwt from 'jsonwebtoken';`,
|
|
``,
|
|
`// Legacy verification for migration`,
|
|
`export function verifyToken(token: string) {`,
|
|
` return jwt.verify(token, process.env.JWT_SECRET!);`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains should suppress the auth chain since clerkMiddleware is present
|
|
const authChainFromNextjs = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "nextjs" && i.targetSkill === "auth" && i.message?.includes("JWT"),
|
|
);
|
|
expect(authChainFromNextjs).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Additional chainTo coverage: vercel-functions chains
|
|
// -------------------------------------------------------------------
|
|
|
|
test("vercel-functions file with direct OpenAI SDK chains to ai-sdk", () => {
|
|
const filePath = "/project/app/api/generate/route.ts";
|
|
const fileContent = [
|
|
`import OpenAI from 'openai';`,
|
|
``,
|
|
`const openai = new OpenAI();`,
|
|
`export async function POST(req: Request) {`,
|
|
` const completion = await openai.chat.completions.create({`,
|
|
` model: 'gpt-5.4',`,
|
|
` messages: [{ role: 'user', content: 'Hello' }],`,
|
|
` });`,
|
|
` return Response.json(completion);`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("vercel-functions");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const aiSdkChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "vercel-functions" && i.targetSkill === "ai-sdk",
|
|
);
|
|
expect(aiSdkChain).toBeDefined();
|
|
expect(aiSdkChain!.message).toContain("AI SDK");
|
|
});
|
|
|
|
test("vercel-functions file with writeFile chains to vercel-storage", () => {
|
|
const filePath = "/project/app/api/upload/route.ts";
|
|
const fileContent = [
|
|
`import { writeFileSync } from 'node:fs';`,
|
|
``,
|
|
`export async function POST(req: Request) {`,
|
|
` const data = await req.arrayBuffer();`,
|
|
` writeFileSync('/tmp/upload.bin', Buffer.from(data));`,
|
|
` return Response.json({ ok: true });`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("vercel-functions");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const storageChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "vercel-functions" && i.targetSkill === "vercel-storage",
|
|
);
|
|
expect(storageChain).toBeDefined();
|
|
expect(storageChain!.message).toContain("Storage");
|
|
});
|
|
|
|
test("vercel-functions file with deprecated AI SDK v5 APIs chains to ai-sdk", () => {
|
|
const filePath = "/project/app/api/extract/route.ts";
|
|
const fileContent = [
|
|
`import { generateObject } from 'ai';`,
|
|
``,
|
|
`export async function POST(req: Request) {`,
|
|
` const result = await generateObject({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` schema: z.object({ name: z.string() }),`,
|
|
` prompt: 'Extract the name',`,
|
|
` });`,
|
|
` return Response.json(result.object);`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("vercel-functions");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const aiSdkChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "vercel-functions" && i.targetSkill === "ai-sdk",
|
|
);
|
|
expect(aiSdkChain).toBeDefined();
|
|
expect(aiSdkChain!.message).toContain("v5");
|
|
});
|
|
|
|
test("vercel-functions polling loop chain is skipped when workflow is already used (skipIfFileContains)", () => {
|
|
const filePath = "/project/app/api/poll/route.ts";
|
|
const fileContent = [
|
|
`'use workflow';`,
|
|
``,
|
|
`while (true) {`,
|
|
` const status = await checkStatus();`,
|
|
` if (status === 'done') break;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains should suppress the workflow chain since 'use workflow' is present
|
|
const workflowChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "vercel-functions" && i.targetSkill === "workflow",
|
|
);
|
|
expect(workflowChain).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Additional chainTo coverage: ai-gateway chains
|
|
// -------------------------------------------------------------------
|
|
|
|
test("ai-gateway file with direct provider SDK import chains to ai-sdk", () => {
|
|
const filePath = "/project/lib/ai.ts";
|
|
const fileContent = [
|
|
`import { anthropic } from '@ai-sdk/anthropic';`,
|
|
`import { generateText } from 'ai';`,
|
|
``,
|
|
`const result = await generateText({`,
|
|
` model: anthropic('claude-sonnet-4.6'),`,
|
|
` prompt: 'Hello!',`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-gateway");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const aiSdkChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-gateway" && i.targetSkill === "ai-sdk",
|
|
);
|
|
expect(aiSdkChain).toBeDefined();
|
|
expect(aiSdkChain!.message).toContain("Gateway");
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Additional chainTo coverage: email chains
|
|
// -------------------------------------------------------------------
|
|
|
|
test("email file with setTimeout chains to workflow", () => {
|
|
const filePath = "/project/lib/email.ts";
|
|
const fileContent = [
|
|
`import { Resend } from 'resend';`,
|
|
``,
|
|
`const resend = new Resend(process.env.RESEND_API_KEY);`,
|
|
``,
|
|
`export async function sendWelcomeEmail(to: string) {`,
|
|
` // Delay the follow-up email`,
|
|
` setTimeout(async () => {`,
|
|
` await resend.emails.send({ from: 'hi@example.com', to, subject: 'Follow up' });`,
|
|
` }, 86400000);`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("email");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const workflowChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "email" && i.targetSkill === "workflow",
|
|
);
|
|
expect(workflowChain).toBeDefined();
|
|
expect(workflowChain!.message).toContain("Workflow");
|
|
});
|
|
|
|
test("email retry chain is skipped when workflow is already used (skipIfFileContains)", () => {
|
|
const filePath = "/project/lib/email.ts";
|
|
const fileContent = [
|
|
`import { createWorkflow } from 'workflow';`,
|
|
`import { Resend } from 'resend';`,
|
|
``,
|
|
`let retries = 0;`,
|
|
`const maxRetries = 3;`,
|
|
`try { await send(); } catch { retry(); }`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains should suppress the retry→workflow chain
|
|
const retryChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "email" && i.targetSkill === "workflow" && i.message?.includes("retry"),
|
|
);
|
|
expect(retryChain).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Additional chainTo coverage: vercel-queues chains
|
|
// -------------------------------------------------------------------
|
|
|
|
test("vercel-queues file with BullMQ chains to workflow", () => {
|
|
const filePath = "/project/lib/queue.ts";
|
|
const fileContent = [
|
|
`import { Queue, Worker } from 'bullmq';`,
|
|
``,
|
|
`const queue = new Queue('email-queue');`,
|
|
`const worker = new Worker('email-queue', async (job) => {`,
|
|
` await sendEmail(job.data);`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("vercel-queues");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const workflowChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "vercel-queues" && i.targetSkill === "workflow",
|
|
);
|
|
expect(workflowChain).toBeDefined();
|
|
expect(workflowChain!.message).toContain("BullMQ");
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Additional chainTo coverage: vercel-firewall chains
|
|
// -------------------------------------------------------------------
|
|
|
|
test("vercel-firewall file with express-rate-limit chains to routing-middleware", () => {
|
|
const filePath = "/project/lib/rate-limit.ts";
|
|
const fileContent = [
|
|
`import rateLimit from 'express-rate-limit';`,
|
|
``,
|
|
`export const limiter = rateLimit({`,
|
|
` windowMs: 15 * 60 * 1000,`,
|
|
` max: 100,`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("vercel-firewall");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const routingChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "vercel-firewall" && i.targetSkill === "routing-middleware",
|
|
);
|
|
expect(routingChain).toBeDefined();
|
|
expect(routingChain!.message).toContain("rate limit");
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Additional chainTo coverage: satori chains
|
|
// -------------------------------------------------------------------
|
|
|
|
test("satori file with puppeteer chains to vercel-functions", () => {
|
|
const filePath = "/project/lib/og.ts";
|
|
const fileContent = [
|
|
`import puppeteer from 'puppeteer';`,
|
|
``,
|
|
`export async function generateOG(title: string) {`,
|
|
` const browser = await puppeteer.launch({ headless: true });`,
|
|
` const page = await browser.newPage();`,
|
|
` await page.setContent('<h1>' + title + '</h1>');`,
|
|
` const screenshot = await page.screenshot();`,
|
|
` await browser.close();`,
|
|
` return screenshot;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("satori");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const functionsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "satori" && i.targetSkill === "vercel-functions",
|
|
);
|
|
expect(functionsChain).toBeDefined();
|
|
expect(functionsChain!.message).toContain("browser");
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Additional chainTo coverage: vercel-flags chains
|
|
// -------------------------------------------------------------------
|
|
|
|
test("vercel-flags file with env var feature flags chains to vercel-storage", () => {
|
|
const filePath = "/project/lib/flags.ts";
|
|
const fileContent = [
|
|
`const isNewUI = process.env.FEATURE_NEW_UI === 'true';`,
|
|
`const isEnabled = process.env.ENABLE_DARK_MODE === '1';`,
|
|
``,
|
|
`export { isNewUI, isEnabled };`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("vercel-flags");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const storageChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "vercel-flags" && i.targetSkill === "vercel-storage",
|
|
);
|
|
expect(storageChain).toBeDefined();
|
|
expect(storageChain!.message).toContain("Edge Config");
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Additional chainTo coverage: workflow → ai-elements
|
|
// -------------------------------------------------------------------
|
|
|
|
test("workflow file with useChat chains to ai-elements (skipIfFileContains guard)", () => {
|
|
const filePath = "/project/app/workflow/chat.tsx";
|
|
const fileContent = [
|
|
`'use client';`,
|
|
`import { useChat } from '@ai-sdk/react';`,
|
|
`import { DefaultChatTransport } from '@ai-sdk/react';`,
|
|
``,
|
|
`export function WorkflowChat() {`,
|
|
` const { messages } = useChat({ transport: new DefaultChatTransport({ api: '/api/workflow/chat' }) });`,
|
|
` return <div>{messages.map(m => <p key={m.id}>{m.content}</p>)}</div>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// Should chain to ai-elements since there's no ai-elements import
|
|
const aiElementsChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "ai-elements",
|
|
);
|
|
if (matched.includes("workflow")) {
|
|
expect(aiElementsChain).toBeDefined();
|
|
expect(aiElementsChain!.message).toContain("AI Elements");
|
|
}
|
|
});
|
|
|
|
test("workflow file with useChat is skipped when MessageResponse already imported", () => {
|
|
const filePath = "/project/app/workflow/chat.tsx";
|
|
const fileContent = [
|
|
`'use client';`,
|
|
`import { useChat } from '@ai-sdk/react';`,
|
|
`import { MessageResponse } from '@/components/ai-elements/message';`,
|
|
``,
|
|
`export function WorkflowChat() {`,
|
|
` const { messages } = useChat({ transport: new DefaultChatTransport({ api: '/api/workflow/chat' }) });`,
|
|
` return <div>{messages.map(m => <MessageResponse key={m.id} content={m.content} />)}</div>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'ai-elements|MessageResponse|<Message\b' should suppress
|
|
const aiElementsFromWorkflow = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "workflow" && i.targetSkill === "ai-elements",
|
|
);
|
|
expect(aiElementsFromWorkflow).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Additional chainTo coverage: geistdocs → nextjs
|
|
// -------------------------------------------------------------------
|
|
|
|
test("geistdocs file with nextra import chains to nextjs", () => {
|
|
const filePath = "/project/next.config.mjs";
|
|
const fileContent = [
|
|
`import nextra from 'nextra';`,
|
|
``,
|
|
`const withNextra = nextra({ theme: 'nextra-theme-docs' });`,
|
|
`export default withNextra({});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("geistdocs");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const nextjsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "geistdocs" && i.targetSkill === "nextjs",
|
|
);
|
|
expect(nextjsChain).toBeDefined();
|
|
expect(nextjsChain!.message).toContain("Nextra");
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Additional chainTo coverage: shadcn → ai-elements (dangerouslySetInnerHTML)
|
|
// -------------------------------------------------------------------
|
|
|
|
test("shadcn file with dangerouslySetInnerHTML chains to ai-elements", () => {
|
|
const filePath = "/project/components/ai-output.tsx";
|
|
const fileContent = [
|
|
`import { cn } from '@/lib/utils';`,
|
|
``,
|
|
`export function AIOutput({ html }: { html: string }) {`,
|
|
` return <div className={cn('prose')} dangerouslySetInnerHTML={{ __html: html }} />;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("shadcn");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const aiElementsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "shadcn" && i.targetSkill === "ai-elements",
|
|
);
|
|
expect(aiElementsChain).toBeDefined();
|
|
expect(aiElementsChain!.message).toContain("HTML");
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Additional chainTo coverage: react-best-practices → shadcn
|
|
// -------------------------------------------------------------------
|
|
|
|
test("react-best-practices file with styled-components chains to shadcn", () => {
|
|
const filePath = "/project/components/Button.tsx";
|
|
const fileContent = [
|
|
`import styled from 'styled-components';`,
|
|
``,
|
|
`const StyledButton = styled.button\``,
|
|
` background: blue;`,
|
|
` color: white;`,
|
|
`\`;`,
|
|
``,
|
|
`export function Button({ children }: { children: React.ReactNode }) {`,
|
|
` return <StyledButton>{children}</StyledButton>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("react-best-practices");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const shadcnChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "react-best-practices" && i.targetSkill === "shadcn",
|
|
);
|
|
expect(shadcnChain).toBeDefined();
|
|
expect(shadcnChain!.message).toContain("shadcn");
|
|
});
|
|
|
|
test("react-best-practices fetch().then() chain to swr is skipped when useSWR present (skipIfFileContains)", () => {
|
|
const filePath = "/project/components/UserList.tsx";
|
|
const fileContent = [
|
|
`import useSWR from 'swr';`,
|
|
``,
|
|
`// Legacy fetch still in codebase`,
|
|
`fetch('/api/old').then(res => res.json());`,
|
|
``,
|
|
`export function UserList() {`,
|
|
` const { data } = useSWR('/api/users', fetcher);`,
|
|
` return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains should suppress since useSWR is present
|
|
const swrChainFromFetch = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "react-best-practices" && i.targetSkill === "swr"
|
|
&& i.message?.includes("fetch"),
|
|
);
|
|
expect(swrChainFromFetch).toBeUndefined();
|
|
});
|
|
// -------------------------------------------------------------------------
|
|
// payments chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("payments file with manual retry logic chains to workflow", () => {
|
|
const filePath = "/project/app/api/checkout/route.ts";
|
|
const fileContent = [
|
|
`import Stripe from 'stripe';`,
|
|
`let retries = 3;`,
|
|
`while (retries > 0) {`,
|
|
` try { await charge(); break; } catch { retries--; }`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const workflowChain = chainResult.injected.find((i) => i.targetSkill === "workflow");
|
|
expect(workflowChain).toBeDefined();
|
|
});
|
|
|
|
test("payments retry chain is skipped when workflow already used (skipIfFileContains)", () => {
|
|
const filePath = "/project/app/api/checkout/route.ts";
|
|
const fileContent = [
|
|
`import { createWorkflow } from 'workflow';`,
|
|
`let retries = 3;`,
|
|
`while (retries > 0) { retries--; }`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const workflowRetryChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "workflow" && i.message?.includes("retry"),
|
|
);
|
|
expect(workflowRetryChain).toBeUndefined();
|
|
});
|
|
|
|
test("payments file with Stripe webhook chains to payments", () => {
|
|
const filePath = "/project/app/api/webhook/route.ts";
|
|
const fileContent = [
|
|
`import Stripe from 'stripe';`,
|
|
`const event = stripe.webhooks.constructEvent(body, sig, STRIPE_WEBHOOK_SECRET);`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const paymentsChain = chainResult.injected.find((i) => i.targetSkill === "payments");
|
|
expect(paymentsChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// cron-jobs chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("cron-jobs file with setTimeout chains to workflow", () => {
|
|
const filePath = "/project/app/api/cron/route.ts";
|
|
const fileContent = [
|
|
`export async function GET() {`,
|
|
` setTimeout(() => processJobs(), 5000);`,
|
|
` return new Response('OK');`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const workflowChain = chainResult.injected.find((i) => i.targetSkill === "workflow");
|
|
expect(workflowChain).toBeDefined();
|
|
});
|
|
|
|
test("cron-jobs file with croner import chains to cron-jobs", () => {
|
|
const filePath = "/project/lib/scheduler.ts";
|
|
const fileContent = `import { Cron } from 'croner';\nconst job = new Cron('0 * * * *', () => {});\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const cronChain = chainResult.injected.find((i) => i.targetSkill === "cron-jobs");
|
|
expect(cronChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// routing-middleware chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("routing-middleware file with next-auth import chains to auth", () => {
|
|
const filePath = "/project/middleware.ts";
|
|
const fileContent = [
|
|
`import { NextResponse } from 'next/server';`,
|
|
`import { getToken } from 'next-auth/jwt';`,
|
|
`export function middleware(req) { return NextResponse.next(); }`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const authChain = chainResult.injected.find((i) => i.targetSkill === "auth");
|
|
expect(authChain).toBeDefined();
|
|
});
|
|
|
|
test("routing-middleware NextResponse chain is skipped in proxy.ts context (skipIfFileContains)", () => {
|
|
const filePath = "/project/proxy.ts";
|
|
const fileContent = [
|
|
`import { NextResponse } from 'next/server';`,
|
|
`// runtime nodejs`,
|
|
`export function proxy(req) { return NextResponse.next(); }`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const nextjsChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "nextjs" && i.message?.includes("proxy.ts"),
|
|
);
|
|
expect(nextjsChain).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// vercel-storage chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("vercel-storage file with @vercel/kv import chains to nextjs", () => {
|
|
const filePath = "/project/lib/cache.ts";
|
|
const fileContent = `import { kv } from '@vercel/kv';\nconst val = await kv.get('key');\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const nextjsChain = chainResult.injected.find((i) => i.targetSkill === "nextjs");
|
|
expect(nextjsChain).toBeDefined();
|
|
});
|
|
|
|
test("vercel-storage file with Supabase import chains to vercel-storage", () => {
|
|
const filePath = "/project/lib/db.ts";
|
|
const fileContent = `import { createClient } from '@supabase/supabase-js';\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const storageChain = chainResult.injected.find((i) => i.targetSkill === "vercel-storage");
|
|
expect(storageChain).toBeDefined();
|
|
});
|
|
|
|
test("vercel-storage file with MongoDB import chains to vercel-storage", () => {
|
|
const filePath = "/project/lib/db.ts";
|
|
const fileContent = `import mongoose from 'mongoose';\nawait mongoose.connect(uri);\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const storageChain = chainResult.injected.find((i) => i.targetSkill === "vercel-storage");
|
|
expect(storageChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// vercel-queues chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("vercel-queues file with SQS import chains to workflow", () => {
|
|
const filePath = "/project/lib/queue.ts";
|
|
const fileContent = `import { SQSClient } from '@aws-sdk/client-sqs';\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const workflowChain = chainResult.injected.find((i) => i.targetSkill === "workflow");
|
|
expect(workflowChain).toBeDefined();
|
|
});
|
|
|
|
test("vercel-queues file with p-queue import chains to vercel-queues", () => {
|
|
const filePath = "/project/lib/workers.ts";
|
|
const fileContent = `import PQueue from 'p-queue';\nconst q = new PQueue({ concurrency: 2 });\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const queuesChain = chainResult.injected.find((i) => i.targetSkill === "vercel-queues");
|
|
expect(queuesChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// chat-sdk chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("chat-sdk file with direct OpenAI import chains to ai-sdk", () => {
|
|
const filePath = "/project/lib/bot.ts";
|
|
const fileContent = [
|
|
`import { Chat } from 'chat';`,
|
|
`import OpenAI from 'openai';`,
|
|
`const openai = new OpenAI();`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const aiSdkChain = chainResult.injected.find((i) => i.targetSkill === "ai-sdk");
|
|
expect(aiSdkChain).toBeDefined();
|
|
});
|
|
|
|
test("chat-sdk file with @slack/bolt import chains to chat-sdk", () => {
|
|
const filePath = "/project/lib/bot.ts";
|
|
const fileContent = [
|
|
`import { App } from '@slack/bolt';`,
|
|
`const app = new App({ token: process.env.SLACK_BOT_TOKEN });`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const chatSdkChain = chainResult.injected.find((i) => i.targetSkill === "chat-sdk");
|
|
expect(chatSdkChain).toBeDefined();
|
|
});
|
|
|
|
test("chat-sdk file with discord.js import chains to chat-sdk", () => {
|
|
const filePath = "/project/lib/discord-bot.ts";
|
|
const fileContent = `import { Client } from 'discord.js';\nconst client = new Client();\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const chatSdkChain = chainResult.injected.find((i) => i.targetSkill === "chat-sdk");
|
|
expect(chatSdkChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// email chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("email file with nodemailer import chains to email", () => {
|
|
const filePath = "/project/lib/mailer.ts";
|
|
const fileContent = [
|
|
`import nodemailer from 'nodemailer';`,
|
|
`const transporter = nodemailer.createTransport({});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const emailChain = chainResult.injected.find((i) => i.targetSkill === "email");
|
|
expect(emailChain).toBeDefined();
|
|
});
|
|
|
|
test("email file with batch send chains to workflow (skipIfFileContains)", () => {
|
|
const filePath = "/project/lib/campaign.ts";
|
|
const fileContent = [
|
|
`const emails = users.map(u => u.email);`,
|
|
`await Promise.all(emails.map(e => sendEmail(e)));`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// If email skill matched, workflow chain should fire for batch patterns
|
|
const workflowChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "workflow" && i.message?.includes("batch"),
|
|
);
|
|
// Whether it fires depends on email skill matching — file may or may not match email pathPatterns
|
|
// The important thing: if it fires, it targets workflow
|
|
if (workflowChain) {
|
|
expect(workflowChain.targetSkill).toBe("workflow");
|
|
}
|
|
});
|
|
|
|
test("email batch chain is skipped when workflow already used", () => {
|
|
const filePath = "/project/lib/campaign.ts";
|
|
const fileContent = [
|
|
`import { createWorkflow } from 'workflow';`,
|
|
`const emails = users.map(u => u.email);`,
|
|
`await Promise.all(emails.map(e => sendEmail(e)));`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const batchChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "workflow" && i.message?.includes("batch"),
|
|
);
|
|
expect(batchChain).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// auth chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("auth file with Vercel OAuth env vars chains to sign-in-with-vercel", () => {
|
|
const filePath = "/project/lib/auth.ts";
|
|
const fileContent = [
|
|
`const clientId = process.env.VERCEL_CLIENT_ID;`,
|
|
`const clientSecret = process.env.VERCEL_CLIENT_SECRET;`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const siVercelChain = chainResult.injected.find((i) => i.targetSkill === "sign-in-with-vercel");
|
|
expect(siVercelChain).toBeDefined();
|
|
});
|
|
|
|
test("auth file with jsonwebtoken import chains to auth", () => {
|
|
const filePath = "/project/lib/auth.ts";
|
|
const fileContent = [
|
|
`import jwt from 'jsonwebtoken';`,
|
|
`const token = jwt.sign({ userId: '123' }, secret);`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const authChain = chainResult.injected.find((i) => i.targetSkill === "auth");
|
|
expect(authChain).toBeDefined();
|
|
});
|
|
|
|
test("auth file with middleware export chains to routing-middleware", () => {
|
|
const filePath = "/project/middleware.ts";
|
|
const fileContent = [
|
|
`import { clerkMiddleware } from '@clerk/nextjs/server';`,
|
|
`export default function middleware(req) { return clerkMiddleware()(req); }`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const routingChain = chainResult.injected.find((i) => i.targetSkill === "routing-middleware");
|
|
expect(routingChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// swr chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("swr file with direct AI provider SDK chains to ai-sdk", () => {
|
|
const filePath = "/project/components/dashboard.tsx";
|
|
const fileContent = [
|
|
`import useSWR from 'swr';`,
|
|
`import { OpenAI } from 'openai';`,
|
|
`const openai = new OpenAI();`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const aiSdkChain = chainResult.injected.find((i) => i.targetSkill === "ai-sdk");
|
|
expect(aiSdkChain).toBeDefined();
|
|
});
|
|
|
|
test("swr file with @vercel/kv chains to vercel-storage", () => {
|
|
const filePath = "/project/lib/data.ts";
|
|
const fileContent = [
|
|
`import useSWR from 'swr';`,
|
|
`import { kv } from '@vercel/kv';`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const storageChain = chainResult.injected.find((i) => i.targetSkill === "vercel-storage");
|
|
expect(storageChain).toBeDefined();
|
|
});
|
|
|
|
test("swr file with useEffect+fetch chains to swr", () => {
|
|
const filePath = "/project/components/list.tsx";
|
|
const fileContent = [
|
|
`'use client';`,
|
|
`import { useEffect, useState } from 'react';`,
|
|
`useEffect(() => {`,
|
|
` fetch('/api/items').then(r => r.json()).then(setItems);`,
|
|
`}, []);`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const swrChain = chainResult.injected.find((i) => i.targetSkill === "swr");
|
|
expect(swrChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ai-elements chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("ai-elements file with raw message.content chains to ai-sdk", () => {
|
|
const filePath = "/project/components/chat.tsx";
|
|
const fileContent = [
|
|
`'use client';`,
|
|
`import { Message } from '@/components/ai-elements/message';`,
|
|
`{message.content}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const aiSdkChain = chainResult.injected.find((i) => i.targetSkill === "ai-sdk");
|
|
expect(aiSdkChain).toBeDefined();
|
|
});
|
|
|
|
test("ai-elements file with react-markdown chains to ai-elements", () => {
|
|
const filePath = "/project/components/response.tsx";
|
|
const fileContent = [
|
|
`import ReactMarkdown from 'react-markdown';`,
|
|
`<ReactMarkdown>{text}</ReactMarkdown>`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const aiElementsChain = chainResult.injected.find((i) => i.targetSkill === "ai-elements");
|
|
expect(aiElementsChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// runtime-cache chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("runtime-cache file with @vercel/kv chains to vercel-storage", () => {
|
|
const filePath = "/project/lib/cache.ts";
|
|
const fileContent = [
|
|
`import { unstable_cache } from 'next/cache';`,
|
|
`import { kv } from '@vercel/kv';`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const storageChain = chainResult.injected.find((i) => i.targetSkill === "vercel-storage");
|
|
expect(storageChain).toBeDefined();
|
|
});
|
|
|
|
test("runtime-cache file with ioredis chains to vercel-storage", () => {
|
|
const filePath = "/project/lib/redis.ts";
|
|
const fileContent = [
|
|
`import Redis from 'ioredis';`,
|
|
`const redis = new Redis();`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const storageChain = chainResult.injected.find((i) => i.targetSkill === "vercel-storage");
|
|
expect(storageChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// vercel-sandbox chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("vercel-sandbox file with vm2 import chains to vercel-sandbox", () => {
|
|
const filePath = "/project/lib/executor.ts";
|
|
const fileContent = [
|
|
`import { VM } from 'vm2';`,
|
|
`const vm = new VM();`,
|
|
`vm.run(userCode);`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const sandboxChain = chainResult.injected.find((i) => i.targetSkill === "vercel-sandbox");
|
|
expect(sandboxChain).toBeDefined();
|
|
});
|
|
|
|
test("vercel-sandbox file with child_process exec chains to ai-sdk", () => {
|
|
const filePath = "/project/lib/executor.ts";
|
|
const fileContent = [
|
|
`import { exec } from 'child_process';`,
|
|
`exec(command, { shell: true }, callback);`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const aiSdkChain = chainResult.injected.find((i) => i.targetSkill === "ai-sdk");
|
|
expect(aiSdkChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ai-gateway chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("ai-gateway file with provider API key chains to ai-sdk", () => {
|
|
const filePath = "/project/lib/ai.ts";
|
|
const fileContent = [
|
|
`import { gateway } from 'ai';`,
|
|
`const key = process.env.ANTHROPIC_API_KEY;`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const aiSdkChain = chainResult.injected.find((i) => i.targetSkill === "ai-sdk");
|
|
expect(aiSdkChain).toBeDefined();
|
|
});
|
|
|
|
test("ai-gateway file with cost tracking tags chains to observability (skipIfFileContains)", () => {
|
|
const filePath = "/project/lib/ai.ts";
|
|
const fileContent = [
|
|
`import { gateway } from 'ai';`,
|
|
`const model = gateway({ tags: ['production'], user: userId });`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const obsChain = chainResult.injected.find((i) => i.targetSkill === "observability");
|
|
expect(obsChain).toBeDefined();
|
|
});
|
|
|
|
test("ai-gateway observability chain is skipped when @vercel/analytics present", () => {
|
|
const filePath = "/project/lib/ai.ts";
|
|
const fileContent = [
|
|
`import { gateway } from 'ai';`,
|
|
`import { track } from '@vercel/analytics';`,
|
|
`const model = gateway({ tags: ['production'], user: userId });`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const obsChain = chainResult.injected.find((i) => i.targetSkill === "observability");
|
|
expect(obsChain).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// vercel-flags chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("vercel-flags file with LaunchDarkly SDK chains to vercel-flags", () => {
|
|
const filePath = "/project/lib/flags.ts";
|
|
const fileContent = `import LaunchDarkly from 'launchdarkly-node-server-sdk';\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const flagsChain = chainResult.injected.find((i) => i.targetSkill === "vercel-flags");
|
|
expect(flagsChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// satori chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("satori file with canvas import chains to vercel-functions", () => {
|
|
const filePath = "/project/lib/og.ts";
|
|
const fileContent = [
|
|
`import { createCanvas } from 'canvas';`,
|
|
`const canvas = createCanvas(1200, 630);`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const functionsChain = chainResult.injected.find((i) => i.targetSkill === "vercel-functions");
|
|
expect(functionsChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// vercel-firewall chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("vercel-firewall file with manual IP blocking chains to routing-middleware", () => {
|
|
const filePath = "/project/middleware.ts";
|
|
const fileContent = [
|
|
`const ip = req.ip;`,
|
|
`const denyList = ['1.2.3.4'];`,
|
|
`if (denyList.includes(ip)) return new Response('Blocked', { status: 403 });`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const routingChain = chainResult.injected.find((i) => i.targetSkill === "routing-middleware");
|
|
expect(routingChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// nextjs additional chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("nextjs file with raw AI fetch URL chains to ai-gateway", () => {
|
|
const filePath = "/project/app/api/chat/route.ts";
|
|
const fileContent = [
|
|
`export async function POST(req: Request) {`,
|
|
` const res = await fetch('https://api.openai.com/v1/chat/completions', {`,
|
|
` headers: { Authorization: 'Bearer ' + key },`,
|
|
` });`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const gatewayChain = chainResult.injected.find((i) => i.targetSkill === "ai-gateway");
|
|
expect(gatewayChain).toBeDefined();
|
|
});
|
|
|
|
test("nextjs raw AI fetch chain is skipped when ai-sdk already imported (skipIfFileContains)", () => {
|
|
const filePath = "/project/app/api/chat/route.ts";
|
|
const fileContent = [
|
|
`import { generateText } from 'ai';`,
|
|
`// legacy: fetch('https://api.openai.com/v1/...')`,
|
|
`const result = await generateText({ model: 'openai/gpt-5.4' });`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const rawFetchChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "ai-gateway" && i.message?.includes("Raw AI provider fetch"),
|
|
);
|
|
expect(rawFetchChain).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// turborepo chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("turborepo file with @vercel/postgres import chains to vercel-storage", () => {
|
|
const filePath = "/project/packages/db/index.ts";
|
|
const fileContent = `import { sql } from '@vercel/postgres';\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const storageChain = chainResult.injected.find((i) => i.targetSkill === "vercel-storage");
|
|
expect(storageChain).toBeDefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Iteration 2: under-covered cross-skill chain rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("vercel-storage file with @vercel/postgres chains to vercel-storage with migration message", () => {
|
|
const filePath = "/project/lib/db.ts";
|
|
const fileContent = `import { sql } from '@vercel/postgres';\nexport const getUsers = () => sql\`SELECT * FROM users\`;\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const storageChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "vercel-storage" && i.message?.includes("sunset"),
|
|
);
|
|
expect(storageChain).toBeDefined();
|
|
});
|
|
|
|
test("vercel-storage @vercel/postgres chain skipped when @neondatabase present", () => {
|
|
const filePath = "/project/lib/db.ts";
|
|
const fileContent = [
|
|
`import { neon } from '@neondatabase/serverless';`,
|
|
`// migrated from @vercel/postgres`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const storageChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "vercel-storage" && i.message?.includes("sunset"),
|
|
);
|
|
expect(storageChain).toBeUndefined();
|
|
});
|
|
|
|
test("auth file with bcrypt import chains to auth with managed-auth message", () => {
|
|
const filePath = "/project/lib/auth.ts";
|
|
const fileContent = [
|
|
`import bcrypt from 'bcrypt';`,
|
|
`export async function hashPassword(pwd: string) {`,
|
|
` return bcrypt.hash(pwd, 10);`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const authChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "auth" && i.message?.includes("managed"),
|
|
);
|
|
expect(authChain).toBeDefined();
|
|
});
|
|
|
|
test("auth file with argon2 import chains to auth with managed-auth message", () => {
|
|
const filePath = "/project/lib/auth.ts";
|
|
const fileContent = `import argon2 from 'argon2';\nconst hash = await argon2.hash(password);\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const authChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "auth" && i.message?.includes("managed"),
|
|
);
|
|
expect(authChain).toBeDefined();
|
|
});
|
|
|
|
test("auth bcrypt chain skipped when @clerk present (skipIfFileContains)", () => {
|
|
const filePath = "/project/lib/auth.ts";
|
|
const fileContent = [
|
|
`import { clerkClient } from '@clerk/nextjs/server';`,
|
|
`// legacy: import bcrypt from 'bcrypt';`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const authChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "auth" && i.message?.includes("bcrypt"),
|
|
);
|
|
expect(authChain).toBeUndefined();
|
|
});
|
|
|
|
test("payments file with paypal import chains to payments with Stripe-marketplace message", () => {
|
|
const filePath = "/project/lib/checkout.ts";
|
|
const fileContent = [
|
|
`import paypal from '@paypal/checkout-server-sdk';`,
|
|
`const client = new paypal.core.PayPalHttpClient(environment);`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const paymentsChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "payments" && i.message?.includes("Stripe"),
|
|
);
|
|
expect(paymentsChain).toBeDefined();
|
|
});
|
|
|
|
test("payments file with braintree import chains to payments with Stripe-marketplace message", () => {
|
|
const filePath = "/project/lib/payments.ts";
|
|
const fileContent = `import braintree from 'braintree';\nconst gateway = new braintree.BraintreeGateway({});\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const paymentsChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "payments" && i.message?.includes("Stripe"),
|
|
);
|
|
expect(paymentsChain).toBeDefined();
|
|
});
|
|
|
|
test("chat-sdk file with @slack/web-api import chains to chat-sdk", () => {
|
|
const filePath = "/project/lib/slack-bot.ts";
|
|
const fileContent = [
|
|
`import { WebClient } from '@slack/web-api';`,
|
|
`const web = new WebClient(process.env.SLACK_BOT_TOKEN);`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const chatSdkChain = chainResult.injected.find((i) => i.targetSkill === "chat-sdk");
|
|
expect(chatSdkChain).toBeDefined();
|
|
});
|
|
|
|
test("observability file with winston import chains to observability with Vercel-native message", () => {
|
|
const filePath = "/project/lib/logger.ts";
|
|
const fileContent = [
|
|
`import winston from 'winston';`,
|
|
`export const logger = winston.createLogger({`,
|
|
` level: 'info',`,
|
|
` transports: [new winston.transports.Console()],`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const obsChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "observability" && i.message?.includes("Vercel"),
|
|
);
|
|
expect(obsChain).toBeDefined();
|
|
});
|
|
|
|
test("observability file with pino import chains to observability", () => {
|
|
const filePath = "/project/lib/logger.ts";
|
|
const fileContent = `import pino from 'pino';\nconst logger = pino();\n`;
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const obsChain = chainResult.injected.find((i) => i.targetSkill === "observability");
|
|
expect(obsChain).toBeDefined();
|
|
});
|
|
|
|
test("observability winston chain skipped when @opentelemetry present", () => {
|
|
const filePath = "/project/lib/logger.ts";
|
|
const fileContent = [
|
|
`import { trace } from '@opentelemetry/api';`,
|
|
`import winston from 'winston';`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const obsChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "observability" && i.message?.includes("winston"),
|
|
);
|
|
expect(obsChain).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Negative tests — files that should NOT trigger chains
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("clean Next.js server component does not trigger any chains", () => {
|
|
const filePath = "/project/app/page.tsx";
|
|
const fileContent = [
|
|
`export default function Page() {`,
|
|
` return <h1>Hello World</h1>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
expect(chainResult.injected.length).toBe(0);
|
|
});
|
|
|
|
test("clean AI SDK usage with gateway does not trigger provider chains", () => {
|
|
const filePath = "/project/app/api/chat/route.ts";
|
|
const fileContent = [
|
|
`import { streamText } from 'ai';`,
|
|
`const result = streamText({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` prompt: 'Hello!',`,
|
|
`});`,
|
|
`return result.toUIMessageStreamResponse();`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// No deprecated patterns, so no chains for outdated API keys or direct providers
|
|
const providerKeyChain = chainResult.injected.find(
|
|
(i) => i.message?.includes("API key") || i.message?.includes("Provider-specific"),
|
|
);
|
|
expect(providerKeyChain).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ncc chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("ncc file with serverless bundle chains to vercel-functions", () => {
|
|
const filePath = "/project/scripts/build.sh";
|
|
const fileContent = [
|
|
`#!/bin/bash`,
|
|
`ncc build api/handler.ts -o dist`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const functionsChain = chainResult.injected.find((i) => i.targetSkill === "vercel-functions");
|
|
if (matched.includes("ncc")) {
|
|
expect(functionsChain).toBeDefined();
|
|
expect(functionsChain!.message).toContain("serverless");
|
|
}
|
|
});
|
|
|
|
test("ncc serverless chain is skipped when vercel.json present (skipIfFileContains)", () => {
|
|
const filePath = "/project/scripts/build.sh";
|
|
const fileContent = [
|
|
`#!/bin/bash`,
|
|
`ncc build api/serverless.ts -o dist`,
|
|
`# vercel.json already configured`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'vercel\.json' should suppress
|
|
const functionsChainFromNcc = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ncc" && i.targetSkill === "vercel-functions" && i.message?.includes("serverless"),
|
|
);
|
|
expect(functionsChainFromNcc).toBeUndefined();
|
|
});
|
|
|
|
test("ncc build chains to deployments-cicd", () => {
|
|
const filePath = "/project/build.ts";
|
|
const fileContent = [
|
|
`import ncc from '@vercel/ncc';`,
|
|
`const { code } = await ncc('./src/index.ts');`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const cicdChain = chainResult.injected.find((i) => i.targetSkill === "deployments-cicd");
|
|
if (matched.includes("ncc")) {
|
|
expect(cicdChain).toBeDefined();
|
|
expect(cicdChain!.message).toContain("deploy");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// cms chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("cms file with getStaticProps chains to nextjs", () => {
|
|
const filePath = "/project/pages/blog/[slug].tsx";
|
|
const fileContent = [
|
|
`import { createClient } from '@sanity/client';`,
|
|
``,
|
|
`export async function getStaticProps({ params }) {`,
|
|
` const post = await client.fetch('*[slug.current == $slug]', params);`,
|
|
` return { props: { post }, revalidate: 60 };`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("cms")) {
|
|
const nextjsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "cms" && i.targetSkill === "nextjs",
|
|
);
|
|
expect(nextjsChain).toBeDefined();
|
|
expect(nextjsChain!.message).toContain("Pages Router");
|
|
}
|
|
});
|
|
|
|
test("cms getStaticProps chain is skipped when App Router patterns present (skipIfFileContains)", () => {
|
|
const filePath = "/project/app/blog/[slug]/page.tsx";
|
|
const fileContent = [
|
|
`import { createClient } from '@sanity/client';`,
|
|
``,
|
|
`export function generateStaticParams() {`,
|
|
` return [{ slug: 'hello' }];`,
|
|
`}`,
|
|
``,
|
|
`// Legacy comment: getStaticProps was here`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'generateStaticParams' should suppress
|
|
const pagesRouterChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "cms" && i.targetSkill === "nextjs" && i.message?.includes("Pages Router"),
|
|
);
|
|
expect(pagesRouterChain).toBeUndefined();
|
|
});
|
|
|
|
test("cms file with revalidatePath chains to runtime-cache", () => {
|
|
const filePath = "/project/app/api/revalidate/route.ts";
|
|
const fileContent = [
|
|
`import { createClient } from 'contentful';`,
|
|
`import { revalidatePath } from 'next/cache';`,
|
|
``,
|
|
`export async function POST(req: Request) {`,
|
|
` revalidatePath('/blog');`,
|
|
` return Response.json({ revalidated: true });`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("cms")) {
|
|
const cacheChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "cms" && i.targetSkill === "runtime-cache",
|
|
);
|
|
expect(cacheChain).toBeDefined();
|
|
expect(cacheChain!.message).toContain("Revalidation");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ai-generation-persistence chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("ai-generation-persistence file with streamText chains to ai-gateway", () => {
|
|
const filePath = "/project/app/api/generate/route.ts";
|
|
const fileContent = [
|
|
`import { streamText } from 'ai';`,
|
|
``,
|
|
`export async function POST(req: Request) {`,
|
|
` const result = streamText({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` prompt: req.body,`,
|
|
` });`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("ai-generation-persistence")) {
|
|
const gatewayChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-generation-persistence" && i.targetSkill === "ai-gateway",
|
|
);
|
|
expect(gatewayChain).toBeDefined();
|
|
expect(gatewayChain!.message).toContain("cost");
|
|
}
|
|
});
|
|
|
|
test("ai-generation-persistence gateway chain is skipped when @ai-sdk/gateway present (skipIfFileContains)", () => {
|
|
const filePath = "/project/app/api/generate/route.ts";
|
|
const fileContent = [
|
|
`import { streamText, gateway } from 'ai';`,
|
|
``,
|
|
`const result = streamText({`,
|
|
` model: gateway('openai/gpt-5.4', { tags: ['prod'] }),`,
|
|
` prompt: 'Hello',`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'gateway(' should suppress
|
|
const gatewayChainFromPersistence = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-generation-persistence" && i.targetSkill === "ai-gateway",
|
|
);
|
|
expect(gatewayChainFromPersistence).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// observability chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("observability file with console.log error handling chains to vercel-functions", () => {
|
|
const filePath = "/project/app/api/data/route.ts";
|
|
const fileContent = [
|
|
`export async function GET(req: Request) {`,
|
|
` try {`,
|
|
` const data = await fetchData();`,
|
|
` return Response.json(data);`,
|
|
` } catch (err) {`,
|
|
` console.log("error", err);`,
|
|
` return Response.json({ error: 'failed' }, { status: 500 });`,
|
|
` }`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("observability")) {
|
|
const functionsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "observability" && i.targetSkill === "vercel-functions",
|
|
);
|
|
expect(functionsChain).toBeDefined();
|
|
expect(functionsChain!.message).toContain("Console.log");
|
|
}
|
|
});
|
|
|
|
test("observability console.log chain is skipped when Sentry present (skipIfFileContains)", () => {
|
|
const filePath = "/project/app/api/data/route.ts";
|
|
const fileContent = [
|
|
`import * as Sentry from '@sentry/nextjs';`,
|
|
``,
|
|
`try { await fetchData(); } catch (err) {`,
|
|
` console.log("error", err);`,
|
|
` Sentry.captureException(err);`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'captureException|@sentry/' should suppress
|
|
const consoleChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "observability" && i.targetSkill === "vercel-functions" && i.message?.includes("Console.log"),
|
|
);
|
|
expect(consoleChain).toBeUndefined();
|
|
});
|
|
|
|
test("observability file with Sentry SDK chains to nextjs", () => {
|
|
const filePath = "/project/sentry.server.config.ts";
|
|
const fileContent = [
|
|
`import * as Sentry from '@sentry/nextjs';`,
|
|
``,
|
|
`Sentry.init({`,
|
|
` dsn: process.env.SENTRY_DSN,`,
|
|
` tracesSampleRate: 1.0,`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("observability")) {
|
|
const nextjsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "observability" && i.targetSkill === "nextjs",
|
|
);
|
|
expect(nextjsChain).toBeDefined();
|
|
expect(nextjsChain!.message).toContain("Sentry");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// sign-in-with-vercel chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("sign-in-with-vercel file with NextAuth chains to auth", () => {
|
|
const filePath = "/project/app/api/auth/route.ts";
|
|
const fileContent = [
|
|
`import NextAuth from 'next-auth';`,
|
|
`import { vercelProvider } from './vercel-provider';`,
|
|
``,
|
|
`const handler = NextAuth({ providers: [vercelProvider] });`,
|
|
`export { handler as GET, handler as POST };`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("sign-in-with-vercel")) {
|
|
const authChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "sign-in-with-vercel" && i.targetSkill === "auth",
|
|
);
|
|
expect(authChain).toBeDefined();
|
|
expect(authChain!.message).toContain("NextAuth");
|
|
}
|
|
});
|
|
|
|
test("sign-in-with-vercel NextAuth chain is skipped when Clerk present (skipIfFileContains)", () => {
|
|
const filePath = "/project/app/api/auth/route.ts";
|
|
const fileContent = [
|
|
`import NextAuth from 'next-auth';`,
|
|
`import { clerkMiddleware } from '@clerk/nextjs/server';`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'clerkMiddleware|@clerk/' should suppress
|
|
const authChainFromSiVercel = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "sign-in-with-vercel" && i.targetSkill === "auth" && i.message?.includes("NextAuth"),
|
|
);
|
|
expect(authChainFromSiVercel).toBeUndefined();
|
|
});
|
|
|
|
test("sign-in-with-vercel VERCEL_CLIENT_ID chains to env-vars", () => {
|
|
const filePath = "/project/lib/vercel-auth.ts";
|
|
const fileContent = [
|
|
`const clientId = process.env.VERCEL_CLIENT_ID;`,
|
|
`const redirectUri = 'https://app.example.com/callback';`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("sign-in-with-vercel")) {
|
|
const envChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "sign-in-with-vercel" && i.targetSkill === "env-vars",
|
|
);
|
|
expect(envChain).toBeDefined();
|
|
expect(envChain!.message).toContain("environment variable");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// json-render chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("json-render file with message.content chains to ai-sdk", () => {
|
|
const filePath = "/project/components/chat-message.tsx";
|
|
const fileContent = [
|
|
`export function ChatMessage({ message }) {`,
|
|
` return <p>{message.content}</p>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("json-render")) {
|
|
const aiSdkChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "json-render" && i.targetSkill === "ai-sdk",
|
|
);
|
|
expect(aiSdkChain).toBeDefined();
|
|
expect(aiSdkChain!.message).toContain("v5");
|
|
}
|
|
});
|
|
|
|
test("json-render file with ReactMarkdown chains to ai-elements", () => {
|
|
const filePath = "/project/components/ai-message.tsx";
|
|
const fileContent = [
|
|
`import ReactMarkdown from 'react-markdown';`,
|
|
``,
|
|
`export function AIMessage({ text }: { text: string }) {`,
|
|
` return <ReactMarkdown>{text}</ReactMarkdown>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("json-render")) {
|
|
const aiElementsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "json-render" && i.targetSkill === "ai-elements",
|
|
);
|
|
expect(aiElementsChain).toBeDefined();
|
|
expect(aiElementsChain!.message).toContain("markdown");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// deployments-cicd chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("deployments-cicd file with node-cron chains to cron-jobs", () => {
|
|
const filePath = "/project/scripts/deploy.ts";
|
|
const fileContent = [
|
|
`import cron from 'node-cron';`,
|
|
``,
|
|
`cron.schedule('0 2 * * *', async () => {`,
|
|
` await deployToProduction();`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("deployments-cicd")) {
|
|
const cronChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "deployments-cicd" && i.targetSkill === "cron-jobs",
|
|
);
|
|
expect(cronChain).toBeDefined();
|
|
expect(cronChain!.message).toContain("cron");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// micro chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("micro file with micro import chains to vercel-functions", () => {
|
|
const filePath = "/project/api/hello.ts";
|
|
const fileContent = [
|
|
`import { send, json } from 'micro';`,
|
|
``,
|
|
`export default async (req, res) => {`,
|
|
` const body = await json(req);`,
|
|
` send(res, 200, { hello: body.name });`,
|
|
`};`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("micro")) {
|
|
const functionsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "micro" && i.targetSkill === "vercel-functions",
|
|
);
|
|
expect(functionsChain).toBeDefined();
|
|
expect(functionsChain!.message).toContain("micro");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// bootstrap chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("bootstrap file with @vercel/postgres chains to vercel-storage", () => {
|
|
const filePath = "/project/lib/db.ts";
|
|
const fileContent = [
|
|
`import { sql } from '@vercel/postgres';`,
|
|
`const POSTGRES_URL = process.env.POSTGRES_URL;`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("bootstrap")) {
|
|
const storageChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "bootstrap" && i.targetSkill === "vercel-storage",
|
|
);
|
|
expect(storageChain).toBeDefined();
|
|
expect(storageChain!.message).toContain("sunset");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// next-forge chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("next-forge file with middleware export chains to routing-middleware", () => {
|
|
const filePath = "/project/apps/app/middleware.ts";
|
|
const fileContent = [
|
|
`import { clerkMiddleware } from '@clerk/nextjs/server';`,
|
|
``,
|
|
`export default function middleware(req) {`,
|
|
` return clerkMiddleware()(req);`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("next-forge")) {
|
|
const routingChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "next-forge" && i.targetSkill === "routing-middleware",
|
|
);
|
|
expect(routingChain).toBeDefined();
|
|
expect(routingChain!.message).toContain("middleware");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Chain cap with >2 newly added rules (DEFAULT_CHAIN_CAP=2 enforcement)
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("chain cap limits injection to 2 when >2 new rules match (default cap)", () => {
|
|
// Simulate a file that triggers chains from 3 different source skills
|
|
// using realistic patterns — we use unit-level test for precise control
|
|
const mod = await import("../hooks/posttooluse-validate.mjs");
|
|
const rci = mod.runChainInjection;
|
|
|
|
const chainMap = new Map([
|
|
["source-1", [{ pattern: "MATCH_1", targetSkill: "micro" }]],
|
|
["source-2", [{ pattern: "MATCH_2", targetSkill: "swr" }]],
|
|
["source-3", [{ pattern: "MATCH_3", targetSkill: "cron-jobs" }]],
|
|
]);
|
|
|
|
const fileContent = `MATCH_1;\nMATCH_2;\nMATCH_3;\n`;
|
|
// No VERCEL_PLUGIN_CHAIN_CAP set — default is 2
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = rci(
|
|
fileContent,
|
|
["source-1", "source-2", "source-3"],
|
|
chainMap,
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(2);
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Dedup prevents re-injection of already-seen chained skills (real-world)
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("dedup prevents re-injection when target skill already seen (real-world scenario)", () => {
|
|
const filePath = "/project/lib/db.ts";
|
|
const fileContent = [
|
|
`import { sql } from '@vercel/postgres';`,
|
|
`export const getUsers = () => sql\`SELECT * FROM users\`;`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
|
|
// Simulate ai-gateway and nextjs already seen — common scenario in a session
|
|
const envWithSeen: any = { VERCEL_PLUGIN_SEEN_SKILLS: "nextjs,vercel-storage,ai-gateway" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, envWithSeen);
|
|
|
|
// nextjs is a common target from vercel-storage's @vercel/postgres chain — should be suppressed
|
|
const nextjsChain = chainResult.injected.find(
|
|
(i) => i.targetSkill === "nextjs",
|
|
);
|
|
expect(nextjsChain).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Phase 1: vercel-cli chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("vercel-cli vercel.json with crons config chains to cron-jobs", () => {
|
|
const filePath = "/project/vercel.json";
|
|
const fileContent = JSON.stringify({
|
|
"crons": [{ "path": "/api/cron", "schedule": "0 * * * *" }],
|
|
}, null, 2);
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("vercel-cli")) {
|
|
const cronChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "vercel-cli" && i.targetSkill === "cron-jobs",
|
|
);
|
|
expect(cronChain).toBeDefined();
|
|
expect(cronChain!.message).toContain("Cron");
|
|
}
|
|
});
|
|
|
|
test("vercel-cli vercel.json with functions config chains to vercel-functions", () => {
|
|
const filePath = "/project/vercel.json";
|
|
const fileContent = JSON.stringify({
|
|
"functions": { "api/**/*.ts": { "maxDuration": 60 } },
|
|
}, null, 2);
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("vercel-cli")) {
|
|
const functionsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "vercel-cli" && i.targetSkill === "vercel-functions",
|
|
);
|
|
expect(functionsChain).toBeDefined();
|
|
expect(functionsChain!.message).toContain("Functions");
|
|
}
|
|
});
|
|
|
|
test("vercel-cli vercel.json with redirects chains to routing-middleware", () => {
|
|
const filePath = "/project/vercel.json";
|
|
const fileContent = JSON.stringify({
|
|
"redirects": [{ "source": "/old", "destination": "/new", "permanent": true }],
|
|
}, null, 2);
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("vercel-cli")) {
|
|
const routingChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "vercel-cli" && i.targetSkill === "routing-middleware",
|
|
);
|
|
expect(routingChain).toBeDefined();
|
|
expect(routingChain!.message).toContain("Routing");
|
|
}
|
|
});
|
|
|
|
test("vercel-cli functions chain is skipped when crons also present (skipIfFileContains)", () => {
|
|
const filePath = "/project/vercel.json";
|
|
const fileContent = JSON.stringify({
|
|
"crons": [{ "path": "/api/cron", "schedule": "0 * * * *" }],
|
|
"functions": { "api/**/*.ts": { "maxDuration": 60 } },
|
|
}, null, 2);
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: '"crons"\s*:' should suppress the functions chain
|
|
const functionsChainFromCli = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "vercel-cli" && i.targetSkill === "vercel-functions",
|
|
);
|
|
expect(functionsChainFromCli).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Phase 1: marketplace chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("marketplace file with Neon env var chains to vercel-storage", () => {
|
|
const filePath = "/project/lib/db.ts";
|
|
const fileContent = [
|
|
`const url = process.env.NEON_DATABASE_URL;`,
|
|
`const pool = new Pool({ connectionString: url });`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("marketplace")) {
|
|
const storageChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "marketplace" && i.targetSkill === "vercel-storage",
|
|
);
|
|
expect(storageChain).toBeDefined();
|
|
expect(storageChain!.message).toContain("Database");
|
|
}
|
|
});
|
|
|
|
test("marketplace file with Clerk env var chains to auth", () => {
|
|
const filePath = "/project/.env.local";
|
|
const fileContent = [
|
|
`CLERK_SECRET_KEY=sk_test_abc123`,
|
|
`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xyz`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("marketplace")) {
|
|
const authChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "marketplace" && i.targetSkill === "auth",
|
|
);
|
|
expect(authChain).toBeDefined();
|
|
expect(authChain!.message).toContain("Clerk");
|
|
}
|
|
});
|
|
|
|
test("marketplace file with Sanity env var chains to cms", () => {
|
|
const filePath = "/project/.env.local";
|
|
const fileContent = [
|
|
`SANITY_PROJECT_ID=abc123`,
|
|
`SANITY_DATASET=production`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("marketplace")) {
|
|
const cmsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "marketplace" && i.targetSkill === "cms",
|
|
);
|
|
expect(cmsChain).toBeDefined();
|
|
expect(cmsChain!.message).toContain("CMS");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Phase 1: v0-dev chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("v0-dev file with shadcn component import chains to shadcn", () => {
|
|
const filePath = "/project/components/generated.tsx";
|
|
const fileContent = [
|
|
`import { Button } from '@/components/ui/button';`,
|
|
`import { Card } from '@/components/ui/card';`,
|
|
``,
|
|
`export function GeneratedUI() {`,
|
|
` return <Card><Button>Click me</Button></Card>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("v0-dev")) {
|
|
const shadcnChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "v0-dev" && i.targetSkill === "shadcn",
|
|
);
|
|
expect(shadcnChain).toBeDefined();
|
|
expect(shadcnChain!.message).toContain("shadcn");
|
|
}
|
|
});
|
|
|
|
test("v0-dev file with AI SDK usage chains to ai-sdk", () => {
|
|
const filePath = "/project/components/chat.tsx";
|
|
const fileContent = [
|
|
`import { useChat } from '@ai-sdk/react';`,
|
|
`import { Button } from '@/components/ui/button';`,
|
|
``,
|
|
`export function Chat() {`,
|
|
` const { messages } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat' }) });`,
|
|
` return <div>{messages.map(m => <p key={m.id}>{m.content}</p>)}</div>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("v0-dev")) {
|
|
const aiSdkChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "v0-dev" && i.targetSkill === "ai-sdk",
|
|
);
|
|
expect(aiSdkChain).toBeDefined();
|
|
expect(aiSdkChain!.message).toContain("AI SDK");
|
|
}
|
|
});
|
|
|
|
test("v0-dev ai-sdk chain is skipped when modern patterns present (skipIfFileContains)", () => {
|
|
const filePath = "/project/components/chat.tsx";
|
|
const fileContent = [
|
|
`import { useChat } from '@ai-sdk/react';`,
|
|
`import { convertToModelMessages } from 'ai';`,
|
|
`import { Button } from '@/components/ui/button';`,
|
|
``,
|
|
`// File already using v6 patterns`,
|
|
`const result = streamText({ model: 'openai/gpt-5.4' });`,
|
|
`return result.toUIMessageStreamResponse();`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'convertToModelMessages|toUIMessageStreamResponse' should suppress
|
|
const aiSdkChainFromV0 = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "v0-dev" && i.targetSkill === "ai-sdk",
|
|
);
|
|
expect(aiSdkChainFromV0).toBeUndefined();
|
|
});
|
|
|
|
test("v0-dev file with next/image import chains to nextjs", () => {
|
|
const filePath = "/project/components/hero.tsx";
|
|
const fileContent = [
|
|
`import Image from 'next/image';`,
|
|
`import { Button } from '@/components/ui/button';`,
|
|
``,
|
|
`export function Hero() {`,
|
|
` return <Image src="/hero.jpg" width={1200} height={630} alt="Hero" />;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("v0-dev")) {
|
|
const nextjsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "v0-dev" && i.targetSkill === "nextjs",
|
|
);
|
|
expect(nextjsChain).toBeDefined();
|
|
expect(nextjsChain!.message).toContain("Next.js");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Phase 1: investigation-mode chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("investigation-mode file with workflow imports chains to workflow", () => {
|
|
const filePath = "/project/workflows/review.ts";
|
|
const fileContent = [
|
|
`import { createWorkflow } from 'workflow';`,
|
|
``,
|
|
`const wf = createWorkflow({ id: 'review' });`,
|
|
`// Debugging: workflow stuck at step 3`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("investigation-mode")) {
|
|
const workflowChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "investigation-mode" && i.targetSkill === "workflow",
|
|
);
|
|
expect(workflowChain).toBeDefined();
|
|
expect(workflowChain!.message).toContain("Workflow");
|
|
}
|
|
});
|
|
|
|
test("investigation-mode file with VERCEL_URL chains to deployments-cicd", () => {
|
|
const filePath = "/project/lib/config.ts";
|
|
const fileContent = [
|
|
`const baseUrl = process.env.VERCEL_URL`,
|
|
` ? \`https://\${process.env.VERCEL_URL}\``,
|
|
` : 'http://localhost:3000';`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("investigation-mode")) {
|
|
const deployChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "investigation-mode" && i.targetSkill === "deployments-cicd",
|
|
);
|
|
expect(deployChain).toBeDefined();
|
|
expect(deployChain!.message).toContain("Deployment");
|
|
}
|
|
});
|
|
|
|
test("investigation-mode deployment chain is skipped when vercel inspect present (skipIfFileContains)", () => {
|
|
const filePath = "/project/scripts/debug.sh";
|
|
const fileContent = [
|
|
`#!/bin/bash`,
|
|
`echo "Checking VERCEL_URL..."`,
|
|
`vercel inspect $DEPLOYMENT_ID`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'vercel\s+inspect|vercel\s+logs' should suppress
|
|
const deployChainFromInvestigation = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "investigation-mode" && i.targetSkill === "deployments-cicd",
|
|
);
|
|
expect(deployChainFromInvestigation).toBeUndefined();
|
|
});
|
|
|
|
test("investigation-mode file with @vercel/analytics chains to observability", () => {
|
|
const filePath = "/project/lib/analytics.ts";
|
|
const fileContent = [
|
|
`import { track } from '@vercel/analytics';`,
|
|
``,
|
|
`export function trackEvent(name: string, data: Record<string, string>) {`,
|
|
` track(name, data);`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("investigation-mode")) {
|
|
const obsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "investigation-mode" && i.targetSkill === "observability",
|
|
);
|
|
expect(obsChain).toBeDefined();
|
|
expect(obsChain!.message).toContain("Observability");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Phase 1: verification chainTo rules
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("verification file with process.env references chains to env-vars", () => {
|
|
const filePath = "/project/app/api/data/route.ts";
|
|
const fileContent = [
|
|
`export async function GET() {`,
|
|
` const apiKey = process.env.API_KEY;`,
|
|
` const dbUrl = process.env.DATABASE_URL;`,
|
|
` return Response.json({ ok: true });`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("verification")) {
|
|
const envChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "verification" && i.targetSkill === "env-vars",
|
|
);
|
|
expect(envChain).toBeDefined();
|
|
expect(envChain!.message).toContain("environment variable");
|
|
}
|
|
});
|
|
|
|
test("verification env-vars chain is skipped when .env.local referenced (skipIfFileContains)", () => {
|
|
const filePath = "/project/app/api/data/route.ts";
|
|
const fileContent = [
|
|
`// Config pulled from .env.local via vercel env pull`,
|
|
`const apiKey = process.env.API_KEY;`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'vercel\s+env\s+pull|\.env\.local' should suppress
|
|
const envChainFromVerification = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "verification" && i.targetSkill === "env-vars",
|
|
);
|
|
expect(envChainFromVerification).toBeUndefined();
|
|
});
|
|
|
|
test("verification file with middleware.ts chains to routing-middleware", () => {
|
|
const filePath = "/project/middleware.ts";
|
|
const fileContent = [
|
|
`import { NextResponse } from 'next/server';`,
|
|
``,
|
|
`export function middleware(req) {`,
|
|
` return NextResponse.next();`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("verification")) {
|
|
const routingChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "verification" && i.targetSkill === "routing-middleware",
|
|
);
|
|
expect(routingChain).toBeDefined();
|
|
expect(routingChain!.message).toContain("Middleware");
|
|
}
|
|
});
|
|
|
|
test("verification file with streamText chains to ai-sdk", () => {
|
|
const filePath = "/project/app/api/chat/route.ts";
|
|
const fileContent = [
|
|
`import { streamText } from 'ai';`,
|
|
``,
|
|
`export async function POST(req: Request) {`,
|
|
` const result = streamText({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` prompt: 'Hello!',`,
|
|
` });`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("verification")) {
|
|
const aiSdkChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "verification" && i.targetSkill === "ai-sdk",
|
|
);
|
|
expect(aiSdkChain).toBeDefined();
|
|
expect(aiSdkChain!.message).toContain("AI SDK");
|
|
}
|
|
});
|
|
|
|
test("verification ai-sdk chain is skipped when modern patterns present (skipIfFileContains)", () => {
|
|
const filePath = "/project/app/api/chat/route.ts";
|
|
const fileContent = [
|
|
`import { streamText } from 'ai';`,
|
|
`import { DefaultChatTransport } from '@ai-sdk/react';`,
|
|
``,
|
|
`const result = streamText({ model: 'openai/gpt-5.4' });`,
|
|
`return result.toUIMessageStreamResponse();`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'toUIMessageStreamResponse|DefaultChatTransport' should suppress
|
|
const aiSdkChainFromVerification = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "verification" && i.targetSkill === "ai-sdk",
|
|
);
|
|
expect(aiSdkChainFromVerification).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Phase 2 enrichment: nextjs → shadcn
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("nextjs file with @/components/ui import chains to shadcn", () => {
|
|
const filePath = "/project/app/dashboard/page.tsx";
|
|
const fileContent = [
|
|
`import { Card } from '@/components/ui/card';`,
|
|
`import { Button } from '@/components/ui/button';`,
|
|
``,
|
|
`export default function Dashboard() {`,
|
|
` return <Card><Button>Save</Button></Card>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("nextjs");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const shadcnChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "nextjs" && i.targetSkill === "shadcn",
|
|
);
|
|
expect(shadcnChain).toBeDefined();
|
|
expect(shadcnChain!.message).toContain("shadcn");
|
|
});
|
|
|
|
test("nextjs shadcn chain is skipped when components.json referenced (skipIfFileContains)", () => {
|
|
const filePath = "/project/app/dashboard/page.tsx";
|
|
const fileContent = [
|
|
`// Project uses shadcn — see components.json`,
|
|
`import { Card } from '@/components/ui/card';`,
|
|
``,
|
|
`export default function Dashboard() {`,
|
|
` return <Card>Content</Card>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'shadcn|components\.json' should suppress
|
|
const shadcnChainFromNextjs = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "nextjs" && i.targetSkill === "shadcn",
|
|
);
|
|
expect(shadcnChainFromNextjs).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Phase 2 enrichment: next-forge → auth
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("next-forge file with Clerk patterns chains to auth", () => {
|
|
const filePath = "/project/apps/app/lib/auth.ts";
|
|
const fileContent = [
|
|
`import { clerkMiddleware } from '@clerk/nextjs/server';`,
|
|
`import { auth } from '@clerk/nextjs/server';`,
|
|
``,
|
|
`export async function getUser() {`,
|
|
` const { userId } = await auth();`,
|
|
` return userId;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("next-forge")) {
|
|
const authChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "next-forge" && i.targetSkill === "auth",
|
|
);
|
|
expect(authChain).toBeDefined();
|
|
expect(authChain!.message).toContain("Clerk");
|
|
}
|
|
});
|
|
|
|
test("next-forge auth chain is skipped when @auth0 present (skipIfFileContains)", () => {
|
|
const filePath = "/project/apps/app/lib/auth.ts";
|
|
const fileContent = [
|
|
`import { clerkMiddleware } from '@clerk/nextjs/server';`,
|
|
`import { auth0 } from '@auth0/nextjs-auth0';`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: '@auth0/|@descope/' should suppress
|
|
const authChainFromNextForge = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "next-forge" && i.targetSkill === "auth",
|
|
);
|
|
expect(authChainFromNextForge).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Phase 2 enrichment: next-forge → payments
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("next-forge file with Stripe import chains to payments", () => {
|
|
const filePath = "/project/apps/app/lib/stripe.ts";
|
|
const fileContent = [
|
|
`import Stripe from 'stripe';`,
|
|
``,
|
|
`export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("next-forge")) {
|
|
const paymentsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "next-forge" && i.targetSkill === "payments",
|
|
);
|
|
expect(paymentsChain).toBeDefined();
|
|
expect(paymentsChain!.message).toContain("Stripe");
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Phase 2 enrichment: next-forge → email
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("next-forge file with Resend import chains to email", () => {
|
|
const filePath = "/project/apps/app/lib/email.ts";
|
|
const fileContent = [
|
|
`import { Resend } from 'resend';`,
|
|
``,
|
|
`export const resend = new Resend(process.env.RESEND_API_KEY);`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
if (matched.includes("next-forge")) {
|
|
const emailChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "next-forge" && i.targetSkill === "email",
|
|
);
|
|
expect(emailChain).toBeDefined();
|
|
expect(emailChain!.message).toContain("Email");
|
|
}
|
|
});
|
|
|
|
test("file with modern Upstash Redis does not trigger @vercel/kv chain", () => {
|
|
const filePath = "/project/lib/cache.ts";
|
|
const fileContent = [
|
|
`import { Redis } from '@upstash/redis';`,
|
|
`const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL });`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// Should not trigger sunset @vercel/kv chain
|
|
const kvChain = chainResult.injected.find(
|
|
(i) => i.message?.includes("@vercel/kv"),
|
|
);
|
|
expect(kvChain).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// AI SDK v5→v6 migration chain rules (ai-sdk chainTo)
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("ai-sdk file with generateObject chains to ai-elements (skipIfFileContains Output.object)", () => {
|
|
const filePath = "/project/app/api/extract/route.ts";
|
|
const fileContent = [
|
|
`import { generateObject } from 'ai';`,
|
|
`import { z } from 'zod';`,
|
|
``,
|
|
`const result = await generateObject({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` schema: z.object({ name: z.string() }),`,
|
|
` prompt: 'Extract',`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-sdk");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const aiElementsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-sdk" && i.targetSkill === "ai-elements" && i.message?.includes("Output.object"),
|
|
);
|
|
expect(aiElementsChain).toBeDefined();
|
|
});
|
|
|
|
test("ai-sdk generateObject chain is skipped when Output.object already present", () => {
|
|
const filePath = "/project/app/api/extract/route.ts";
|
|
const fileContent = [
|
|
`import { generateText, Output } from 'ai';`,
|
|
``,
|
|
`const result = await generateText({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` output: Output.object({ schema }),`,
|
|
` prompt: 'Extract',`,
|
|
`});`,
|
|
`// Legacy reference: generateObject was here`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const genObjChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-sdk" && i.message?.includes("Output.object"),
|
|
);
|
|
expect(genObjChain).toBeUndefined();
|
|
});
|
|
|
|
test("ai-sdk file with maxSteps chains to ai-elements (skipIfFileContains stepCountIs)", () => {
|
|
const filePath = "/project/app/api/agent/route.ts";
|
|
const fileContent = [
|
|
`import { streamText } from 'ai';`,
|
|
``,
|
|
`const result = streamText({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` maxSteps: 5,`,
|
|
` prompt: 'Plan a trip',`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-sdk");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const maxStepsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-sdk" && i.message?.includes("stepCountIs"),
|
|
);
|
|
expect(maxStepsChain).toBeDefined();
|
|
});
|
|
|
|
test("ai-sdk maxSteps chain is skipped when stepCountIs already present", () => {
|
|
const filePath = "/project/app/api/agent/route.ts";
|
|
const fileContent = [
|
|
`import { streamText, stepCountIs } from 'ai';`,
|
|
``,
|
|
`const result = streamText({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` stopWhen: stepCountIs(5),`,
|
|
` prompt: 'Plan a trip',`,
|
|
`});`,
|
|
`// Legacy comment: maxSteps: 5`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const maxStepsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-sdk" && i.message?.includes("stepCountIs"),
|
|
);
|
|
expect(maxStepsChain).toBeUndefined();
|
|
});
|
|
|
|
test("ai-sdk file with toDataStreamResponse chains to ai-elements (skipIfFileContains toUIMessageStreamResponse)", () => {
|
|
const filePath = "/project/app/api/chat/route.ts";
|
|
const fileContent = [
|
|
`import { streamText } from 'ai';`,
|
|
``,
|
|
`export async function POST(req: Request) {`,
|
|
` const result = streamText({ model: 'openai/gpt-5.4', prompt: 'Hello' });`,
|
|
` return result.toDataStreamResponse();`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-sdk");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const tdsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-sdk" && i.targetSkill === "ai-elements" && i.message?.includes("toUIMessageStreamResponse"),
|
|
);
|
|
expect(tdsChain).toBeDefined();
|
|
});
|
|
|
|
test("ai-sdk toDataStreamResponse chain is skipped when toUIMessageStreamResponse present", () => {
|
|
const filePath = "/project/app/api/chat/route.ts";
|
|
const fileContent = [
|
|
`import { streamText } from 'ai';`,
|
|
`// Migrated from toDataStreamResponse`,
|
|
`return result.toUIMessageStreamResponse();`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const tdsChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-sdk" && i.message?.includes("toUIMessageStreamResponse") && i.message?.includes("v5"),
|
|
);
|
|
expect(tdsChain).toBeUndefined();
|
|
});
|
|
|
|
test("ai-sdk file with handleSubmit chains to ai-elements (skipIfFileContains sendMessage)", () => {
|
|
const filePath = "/project/components/chat.tsx";
|
|
const fileContent = [
|
|
`'use client';`,
|
|
`import { useChat } from '@ai-sdk/react';`,
|
|
``,
|
|
`export function Chat() {`,
|
|
` const { messages, input, handleSubmit } = useChat();`,
|
|
` return <form onSubmit={handleSubmit}><input value={input} /></form>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-sdk");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const handleSubmitChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-sdk" && i.targetSkill === "ai-elements" && i.message?.includes("sendMessage"),
|
|
);
|
|
expect(handleSubmitChain).toBeDefined();
|
|
});
|
|
|
|
test("ai-sdk handleSubmit chain is skipped when sendMessage already present", () => {
|
|
const filePath = "/project/components/chat.tsx";
|
|
const fileContent = [
|
|
`'use client';`,
|
|
`import { useChat } from '@ai-sdk/react';`,
|
|
`const { sendMessage } = useChat();`,
|
|
`// Legacy: handleSubmit reference in comment`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const handleSubmitChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-sdk" && i.message?.includes("sendMessage") && i.targetSkill === "ai-elements",
|
|
);
|
|
expect(handleSubmitChain).toBeUndefined();
|
|
});
|
|
|
|
test("ai-sdk file with useChat({ api: }) v5 pattern chains to ai-elements", () => {
|
|
const filePath = "/project/components/chat.tsx";
|
|
const fileContent = [
|
|
`'use client';`,
|
|
`import { useChat } from '@ai-sdk/react';`,
|
|
``,
|
|
`export function Chat() {`,
|
|
` const { messages } = useChat({ api: '/api/chat' });`,
|
|
` return <div>{messages.map(m => <p key={m.id}>{m.content}</p>)}</div>;`,
|
|
`}`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-sdk");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const useChatV5Chain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-sdk" && i.targetSkill === "ai-elements" && i.message?.includes("DefaultChatTransport"),
|
|
);
|
|
expect(useChatV5Chain).toBeDefined();
|
|
});
|
|
|
|
test("ai-sdk useChat v5 api chain is skipped when DefaultChatTransport present", () => {
|
|
const filePath = "/project/components/chat.tsx";
|
|
const fileContent = [
|
|
`'use client';`,
|
|
`import { useChat, DefaultChatTransport } from '@ai-sdk/react';`,
|
|
`const { messages } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat' }) });`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const useChatV5Chain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-sdk" && i.message?.includes("DefaultChatTransport") && i.message?.includes("v5"),
|
|
);
|
|
expect(useChatV5Chain).toBeUndefined();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// AI Gateway model deprecation chain rules (ai-gateway chainTo)
|
|
// -------------------------------------------------------------------------
|
|
|
|
test("ai-gateway file with gpt-4o model string chains to ai-sdk", () => {
|
|
const filePath = "/project/lib/ai.ts";
|
|
const fileContent = [
|
|
`import { generateText } from 'ai';`,
|
|
``,
|
|
`const result = await generateText({`,
|
|
` model: 'openai/gpt-4o',`,
|
|
` prompt: 'Hello!',`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-gateway");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const gpt4oChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-gateway" && i.message?.includes("gpt-4o"),
|
|
);
|
|
expect(gpt4oChain).toBeDefined();
|
|
expect(gpt4oChain!.message).toContain("gpt-5.4");
|
|
});
|
|
|
|
test("ai-gateway gpt-4o chain is skipped when gpt-5 already present", () => {
|
|
const filePath = "/project/lib/ai.ts";
|
|
const fileContent = [
|
|
`import { generateText } from 'ai';`,
|
|
`// Migrated from gpt-4o`,
|
|
`const result = await generateText({ model: 'openai/gpt-5.4' });`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const gpt4oChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-gateway" && i.message?.includes("gpt-4o"),
|
|
);
|
|
expect(gpt4oChain).toBeUndefined();
|
|
});
|
|
|
|
test("ai-gateway file with DALL-E reference chains to ai-sdk", () => {
|
|
const filePath = "/project/app/api/image/route.ts";
|
|
const fileContent = [
|
|
`import { generateText } from 'ai';`,
|
|
``,
|
|
`const result = await generateText({`,
|
|
` model: 'openai/dall-e-3',`,
|
|
` prompt: 'A sunset',`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-gateway");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const dalleChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-gateway" && i.message?.includes("DALL-E"),
|
|
);
|
|
expect(dalleChain).toBeDefined();
|
|
expect(dalleChain!.message).toContain("gemini-3.1-flash-image-preview");
|
|
});
|
|
|
|
test("ai-gateway DALL-E chain is skipped when gemini-3 present", () => {
|
|
const filePath = "/project/app/api/image/route.ts";
|
|
const fileContent = [
|
|
`import { generateText } from 'ai';`,
|
|
`// Migrated from dall-e-3`,
|
|
`const result = await generateText({ model: 'google/gemini-3.1-flash-image-preview' });`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const dalleChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-gateway" && i.message?.includes("DALL-E"),
|
|
);
|
|
expect(dalleChain).toBeUndefined();
|
|
});
|
|
|
|
test("ai-gateway file with gemini-2.x model chains to ai-sdk", () => {
|
|
const filePath = "/project/app/api/image/route.ts";
|
|
const fileContent = [
|
|
`import { generateText } from 'ai';`,
|
|
``,
|
|
`const result = await generateText({`,
|
|
` model: 'google/gemini-2.0-flash-exp-image-generation',`,
|
|
` prompt: 'Generate an image',`,
|
|
`});`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
expect(matched).toContain("ai-gateway");
|
|
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const gemini2Chain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-gateway" && i.message?.includes("Gemini 2.x"),
|
|
);
|
|
expect(gemini2Chain).toBeDefined();
|
|
expect(gemini2Chain!.message).toContain("gemini-3");
|
|
});
|
|
|
|
test("ai-gateway gemini-2.x chain is skipped when gemini-3 present", () => {
|
|
const filePath = "/project/app/api/image/route.ts";
|
|
const fileContent = [
|
|
`import { generateText } from 'ai';`,
|
|
`// Old: gemini-2.0-flash-exp-image-generation`,
|
|
`const result = await generateText({ model: 'google/gemini-3.1-flash-image-preview' });`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
const gemini2Chain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-gateway" && i.message?.includes("Gemini 2.x"),
|
|
);
|
|
expect(gemini2Chain).toBeUndefined();
|
|
});
|
|
|
|
test("ai-gateway provider API key chain has skipIfFileContains for OIDC", () => {
|
|
const filePath = "/project/lib/ai.ts";
|
|
const fileContent = [
|
|
`import { gateway } from 'ai';`,
|
|
`// Using OIDC — no manual keys`,
|
|
`const token = process.env.VERCEL_OIDC_TOKEN;`,
|
|
`// Legacy reference: ANTHROPIC_API_KEY was here`,
|
|
].join("\n");
|
|
|
|
const matched = matchFileToSkills(filePath, fileContent, data.compiledSkills, data.rulesMap, undefined, data.chainMap);
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const chainResult = runChainInjection(fileContent, matched, data.chainMap, null, ROOT, undefined, cleanEnv);
|
|
|
|
// skipIfFileContains: 'VERCEL_OIDC|@ai-sdk/gateway|gateway(' should suppress
|
|
const apiKeyChain = chainResult.injected.find(
|
|
(i) => i.sourceSkill === "ai-gateway" && i.message?.includes("Provider-specific API key"),
|
|
);
|
|
expect(apiKeyChain).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// formatOutput with chainResult
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("formatOutput with chain injection", () => {
|
|
let formatOutput: typeof import("../hooks/src/posttooluse-validate.mts").formatOutput;
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import("../hooks/posttooluse-validate.mjs");
|
|
formatOutput = mod.formatOutput;
|
|
});
|
|
|
|
test("chain-only output (no violations) produces additionalContext", () => {
|
|
const chainResult = {
|
|
injected: [
|
|
{
|
|
sourceSkill: "ai-sdk",
|
|
targetSkill: "ai-gateway",
|
|
message: "Direct API key detected.",
|
|
content: "# AI Gateway\n\nUse OIDC for auth.",
|
|
},
|
|
],
|
|
totalBytes: 40,
|
|
};
|
|
|
|
const result = formatOutput([], ["ai-sdk"], "/test/file.ts", undefined, "claude-code", undefined, chainResult);
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.hookSpecificOutput).toBeDefined();
|
|
const ctx = parsed.hookSpecificOutput.additionalContext;
|
|
|
|
// Chain markers present
|
|
expect(ctx).toContain("<!-- posttooluse-chain: ai-sdk → ai-gateway -->");
|
|
expect(ctx).toContain("<!-- /posttooluse-chain: ai-gateway -->");
|
|
expect(ctx).toContain("**Skill context auto-loaded** (ai-gateway):");
|
|
expect(ctx).toContain("Direct API key detected.");
|
|
expect(ctx).toContain("# AI Gateway");
|
|
|
|
// Metadata
|
|
const meta = extractPostValidation(parsed.hookSpecificOutput);
|
|
expect(meta).toBeDefined();
|
|
expect(meta.chainedSkills).toEqual(["ai-gateway"]);
|
|
expect(meta.errorCount).toBe(0);
|
|
});
|
|
|
|
test("violations + chains appear together in additionalContext", () => {
|
|
const violations = [{
|
|
skill: "ai-sdk",
|
|
line: 3,
|
|
message: "Use @ai-sdk/openai provider",
|
|
severity: "error" as const,
|
|
matchedText: "import OpenAI from 'openai'",
|
|
}];
|
|
|
|
const chainResult = {
|
|
injected: [
|
|
{
|
|
sourceSkill: "ai-sdk",
|
|
targetSkill: "ai-gateway",
|
|
content: "# AI Gateway\n\nGateway docs here.",
|
|
},
|
|
],
|
|
totalBytes: 35,
|
|
};
|
|
|
|
const result = formatOutput(violations, ["ai-sdk"], "/test/file.ts", undefined, "claude-code", undefined, chainResult);
|
|
const parsed = JSON.parse(result);
|
|
const ctx = parsed.hookSpecificOutput.additionalContext;
|
|
|
|
// Both validation and chain content present
|
|
expect(ctx).toContain("VALIDATION");
|
|
expect(ctx).toContain("[ERROR]");
|
|
expect(ctx).toContain("<!-- posttooluse-chain: ai-sdk → ai-gateway -->");
|
|
expect(ctx).toContain("# AI Gateway");
|
|
|
|
const meta = extractPostValidation(parsed.hookSpecificOutput);
|
|
expect(meta.errorCount).toBe(1);
|
|
expect(meta.chainedSkills).toEqual(["ai-gateway"]);
|
|
});
|
|
|
|
test("no violations and no chains returns empty JSON", () => {
|
|
const result = formatOutput([], ["ai-sdk"], "/test/file.ts", undefined, "claude-code", undefined, { injected: [], totalBytes: 0 });
|
|
expect(result).toBe("{}");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Integration tests (full hook process spawn)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("posttooluse-chain integration", () => {
|
|
let tmpDir: string;
|
|
let testFile: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = join(tmpdir(), `posttooluse-chain-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
mkdirSync(tmpDir, { recursive: true });
|
|
testFile = join(tmpDir, "app", "api", "chat", "route.ts");
|
|
mkdirSync(join(tmpDir, "app", "api", "chat"), { recursive: true });
|
|
});
|
|
|
|
test("direct provider key in AI SDK file triggers ai-gateway chain injection", async () => {
|
|
writeFileSync(testFile, [
|
|
`import { generateText } from 'ai';`,
|
|
``,
|
|
`const key = process.env.OPENAI_API_KEY;`,
|
|
`const result = await generateText({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` prompt: 'Hello!',`,
|
|
`});`,
|
|
].join("\n"));
|
|
|
|
const { code, parsed, ctx } = await runHook({
|
|
tool_name: "Write",
|
|
tool_input: { file_path: testFile },
|
|
});
|
|
|
|
expect(code).toBe(0);
|
|
// Should have chain injection for ai-gateway
|
|
if (ctx) {
|
|
// The ai-sdk skill's chainTo should fire for OPENAI_API_KEY
|
|
const meta = extractPostValidation(parsed.hookSpecificOutput);
|
|
if (meta?.chainedSkills?.length > 0) {
|
|
expect(meta.chainedSkills).toContain("ai-gateway");
|
|
expect(ctx).toContain("posttooluse-chain:");
|
|
}
|
|
}
|
|
});
|
|
|
|
test("toDataStreamResponse triggers chain injection", async () => {
|
|
writeFileSync(testFile, [
|
|
`import { streamText } from 'ai';`,
|
|
``,
|
|
`export async function POST(req: Request) {`,
|
|
` const result = streamText({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` prompt: 'Hello!',`,
|
|
` });`,
|
|
` return result.toDataStreamResponse();`,
|
|
`}`,
|
|
].join("\n"));
|
|
|
|
const { code, ctx } = await runHook({
|
|
tool_name: "Write",
|
|
tool_input: { file_path: testFile },
|
|
});
|
|
|
|
expect(code).toBe(0);
|
|
// ai-sdk skill should match via import, and chainTo for toDataStreamResponse should fire
|
|
if (ctx) {
|
|
const hasChain = ctx.includes("posttooluse-chain:");
|
|
const hasValidation = ctx.includes("VALIDATION");
|
|
// Should have at least validation or chain output
|
|
expect(hasChain || hasValidation).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("clean file with no deprecated patterns produces no chain injection", async () => {
|
|
writeFileSync(testFile, [
|
|
`import { generateText } from 'ai';`,
|
|
`import { useChat } from '@ai-sdk/react';`,
|
|
``,
|
|
`const result = await generateText({`,
|
|
` model: 'openai/gpt-5.4',`,
|
|
` prompt: 'Hello!',`,
|
|
`});`,
|
|
].join("\n"));
|
|
|
|
const { code, ctx } = await runHook({
|
|
tool_name: "Write",
|
|
tool_input: { file_path: testFile },
|
|
});
|
|
|
|
expect(code).toBe(0);
|
|
// No chain markers expected for clean code
|
|
expect(ctx).not.toContain("posttooluse-chain:");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PostToolUse Bash chain: package install detection
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("posttooluse-bash-chain: parseInstallCommand", () => {
|
|
let parseInstallCommand: typeof import("../hooks/src/posttooluse-bash-chain.mts").parseInstallCommand;
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import("../hooks/posttooluse-bash-chain.mjs");
|
|
parseInstallCommand = mod.parseInstallCommand;
|
|
});
|
|
|
|
test("parses npm install with single package", () => {
|
|
expect(parseInstallCommand("npm install express")).toEqual(["express"]);
|
|
});
|
|
|
|
test("parses npm i shorthand", () => {
|
|
expect(parseInstallCommand("npm i express")).toEqual(["express"]);
|
|
});
|
|
|
|
test("parses yarn add with multiple packages", () => {
|
|
expect(parseInstallCommand("yarn add express mongoose")).toEqual(["express", "mongoose"]);
|
|
});
|
|
|
|
test("parses pnpm add with scoped package", () => {
|
|
expect(parseInstallCommand("pnpm add @vercel/postgres")).toEqual(["@vercel/postgres"]);
|
|
});
|
|
|
|
test("parses bun add", () => {
|
|
expect(parseInstallCommand("bun add openai")).toEqual(["openai"]);
|
|
});
|
|
|
|
test("strips version specifiers from unscoped packages", () => {
|
|
expect(parseInstallCommand("npm install express@latest")).toEqual(["express"]);
|
|
expect(parseInstallCommand("npm install express@^4.0.0")).toEqual(["express"]);
|
|
});
|
|
|
|
test("strips version specifiers from scoped packages", () => {
|
|
expect(parseInstallCommand("npm install @vercel/postgres@0.10.0")).toEqual(["@vercel/postgres"]);
|
|
});
|
|
|
|
test("filters out flags like --save-dev and -D", () => {
|
|
expect(parseInstallCommand("npm install --save-dev express")).toEqual(["express"]);
|
|
expect(parseInstallCommand("npm install -D express")).toEqual(["express"]);
|
|
});
|
|
|
|
test("returns empty array for non-install commands", () => {
|
|
expect(parseInstallCommand("npm run dev")).toEqual([]);
|
|
expect(parseInstallCommand("git commit -m 'test'")).toEqual([]);
|
|
expect(parseInstallCommand("ls -la")).toEqual([]);
|
|
});
|
|
|
|
test("returns empty array for empty or null input", () => {
|
|
expect(parseInstallCommand("")).toEqual([]);
|
|
expect(parseInstallCommand(null as any)).toEqual([]);
|
|
});
|
|
|
|
test("handles npm install with no packages (bare install)", () => {
|
|
// "npm install" with no packages — the regex requires at least one token after install
|
|
// but the trailing space matches empty, which gets filtered
|
|
const result = parseInstallCommand("npm install");
|
|
// Should not crash; result depends on regex behavior
|
|
expect(Array.isArray(result)).toBe(true);
|
|
});
|
|
|
|
test("filters out path arguments", () => {
|
|
expect(parseInstallCommand("npm install ./local-pkg ../other-pkg")).toEqual([]);
|
|
expect(parseInstallCommand("npm install /absolute/path")).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("posttooluse-bash-chain: runBashChainInjection", () => {
|
|
let runBashChainInjection: typeof import("../hooks/src/posttooluse-bash-chain.mts").runBashChainInjection;
|
|
let PACKAGE_SKILL_MAP: typeof import("../hooks/src/posttooluse-bash-chain.mts").PACKAGE_SKILL_MAP;
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import("../hooks/posttooluse-bash-chain.mjs");
|
|
runBashChainInjection = mod.runBashChainInjection;
|
|
PACKAGE_SKILL_MAP = mod.PACKAGE_SKILL_MAP;
|
|
});
|
|
|
|
test("express maps to vercel-functions", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(
|
|
["express"],
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-functions");
|
|
expect(result.injected[0].packageName).toBe("express");
|
|
expect(result.injected[0].content.length).toBeGreaterThan(0);
|
|
expect(result.totalBytes).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("bullmq maps to vercel-queues", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(
|
|
["bullmq"],
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-queues");
|
|
});
|
|
|
|
test("mongoose maps to vercel-storage", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(
|
|
["mongoose"],
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-storage");
|
|
});
|
|
|
|
test("@vercel/postgres maps to vercel-storage", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(
|
|
["@vercel/postgres"],
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-storage");
|
|
expect(result.injected[0].message).toContain("sunset");
|
|
});
|
|
|
|
test("openai maps to ai-gateway", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(
|
|
["openai"],
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("ai-gateway");
|
|
});
|
|
|
|
test("already-seen skill is NOT re-injected", () => {
|
|
const envWithSeen: any = { VERCEL_PLUGIN_SEEN_SKILLS: "vercel-functions" };
|
|
const result = runBashChainInjection(
|
|
["express"],
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
envWithSeen,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(0);
|
|
});
|
|
|
|
test("unknown package produces no injection", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(
|
|
["lodash"],
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(0);
|
|
expect(result.totalBytes).toBe(0);
|
|
});
|
|
|
|
test("multiple packages mapping to same skill only inject once", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(
|
|
["express", "fastify"], // both map to vercel-functions
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-functions");
|
|
});
|
|
|
|
test("chain cap is respected", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "", VERCEL_PLUGIN_CHAIN_CAP: "1" };
|
|
const result = runBashChainInjection(
|
|
["express", "openai", "mongoose"], // 3 different skills
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.injected.length).toBe(1);
|
|
});
|
|
|
|
test("byte budget is respected", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "", VERCEL_PLUGIN_CHAIN_CAP: "10" };
|
|
const result = runBashChainInjection(
|
|
["express", "openai", "mongoose", "bullmq", "swr"],
|
|
null,
|
|
ROOT,
|
|
undefined,
|
|
cleanEnv,
|
|
);
|
|
|
|
expect(result.totalBytes).toBeLessThanOrEqual(18_000);
|
|
});
|
|
|
|
test("PACKAGE_SKILL_MAP has required entries", () => {
|
|
expect(PACKAGE_SKILL_MAP["express"]?.skill).toBe("vercel-functions");
|
|
expect(PACKAGE_SKILL_MAP["bullmq"]?.skill).toBe("vercel-queues");
|
|
expect(PACKAGE_SKILL_MAP["mongoose"]?.skill).toBe("vercel-storage");
|
|
expect(PACKAGE_SKILL_MAP["@vercel/postgres"]?.skill).toBe("vercel-storage");
|
|
expect(PACKAGE_SKILL_MAP["openai"]?.skill).toBe("ai-gateway");
|
|
// New entries
|
|
expect(PACKAGE_SKILL_MAP["prisma"]?.skill).toBe("vercel-storage");
|
|
expect(PACKAGE_SKILL_MAP["@libsql/client"]?.skill).toBe("vercel-storage");
|
|
expect(PACKAGE_SKILL_MAP["stripe"]?.skill).toBe("payments");
|
|
expect(PACKAGE_SKILL_MAP["langchain"]?.skill).toBe("ai-sdk");
|
|
expect(PACKAGE_SKILL_MAP["@clerk/nextjs"]?.skill).toBe("auth");
|
|
expect(PACKAGE_SKILL_MAP["@sanity/client"]?.skill).toBe("cms");
|
|
expect(PACKAGE_SKILL_MAP["contentful"]?.skill).toBe("cms");
|
|
expect(PACKAGE_SKILL_MAP["resend"]?.skill).toBe("email");
|
|
// Remaining entries
|
|
expect(PACKAGE_SKILL_MAP["fastify"]?.skill).toBe("vercel-functions");
|
|
expect(PACKAGE_SKILL_MAP["koa"]?.skill).toBe("vercel-functions");
|
|
expect(PACKAGE_SKILL_MAP["bull"]?.skill).toBe("vercel-queues");
|
|
expect(PACKAGE_SKILL_MAP["@vercel/kv"]?.skill).toBe("vercel-storage");
|
|
expect(PACKAGE_SKILL_MAP["@anthropic-ai/sdk"]?.skill).toBe("ai-gateway");
|
|
expect(PACKAGE_SKILL_MAP["@google/generative-ai"]?.skill).toBe("ai-gateway");
|
|
expect(PACKAGE_SKILL_MAP["@langchain/core"]?.skill).toBe("ai-sdk");
|
|
expect(PACKAGE_SKILL_MAP["workflow"]?.skill).toBe("workflow");
|
|
expect(PACKAGE_SKILL_MAP["ai"]?.skill).toBe("ai-sdk");
|
|
expect(PACKAGE_SKILL_MAP["@ai-sdk/react"]?.skill).toBe("ai-sdk");
|
|
expect(PACKAGE_SKILL_MAP["@vercel/flags"]?.skill).toBe("vercel-flags");
|
|
expect(PACKAGE_SKILL_MAP["swr"]?.skill).toBe("swr");
|
|
expect(PACKAGE_SKILL_MAP["node-cron"]?.skill).toBe("cron-jobs");
|
|
expect(PACKAGE_SKILL_MAP["cron"]?.skill).toBe("cron-jobs");
|
|
// Iteration 2 entries
|
|
expect(PACKAGE_SKILL_MAP["next-auth"]?.skill).toBe("auth");
|
|
expect(PACKAGE_SKILL_MAP["@slack/bolt"]?.skill).toBe("chat-sdk");
|
|
expect(PACKAGE_SKILL_MAP["@slack/web-api"]?.skill).toBe("chat-sdk");
|
|
expect(PACKAGE_SKILL_MAP["discord.js"]?.skill).toBe("chat-sdk");
|
|
expect(PACKAGE_SKILL_MAP["telegraf"]?.skill).toBe("chat-sdk");
|
|
expect(PACKAGE_SKILL_MAP["grammy"]?.skill).toBe("chat-sdk");
|
|
expect(PACKAGE_SKILL_MAP["helmet"]?.skill).toBe("vercel-firewall");
|
|
expect(PACKAGE_SKILL_MAP["cors"]?.skill).toBe("routing-middleware");
|
|
expect(PACKAGE_SKILL_MAP["dotenv"]?.skill).toBe("env-vars");
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Individual injection tests for every PACKAGE_SKILL_MAP entry
|
|
// -------------------------------------------------------------------
|
|
|
|
test("prisma maps to vercel-storage", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["prisma"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-storage");
|
|
expect(result.injected[0].message).toContain("Neon");
|
|
});
|
|
|
|
test("@libsql/client maps to vercel-storage", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["@libsql/client"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-storage");
|
|
});
|
|
|
|
test("stripe maps to payments", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["stripe"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("payments");
|
|
expect(result.injected[0].message).toContain("Stripe");
|
|
});
|
|
|
|
test("langchain maps to ai-sdk", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["langchain"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("ai-sdk");
|
|
expect(result.injected[0].message).toContain("LangChain");
|
|
});
|
|
|
|
test("@langchain/core maps to ai-sdk", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["@langchain/core"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("ai-sdk");
|
|
expect(result.injected[0].message).toContain("LangChain");
|
|
});
|
|
|
|
test("@clerk/nextjs maps to auth", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["@clerk/nextjs"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("auth");
|
|
expect(result.injected[0].message).toContain("Clerk");
|
|
});
|
|
|
|
test("@anthropic-ai/sdk maps to ai-gateway", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["@anthropic-ai/sdk"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("ai-gateway");
|
|
expect(result.injected[0].message).toContain("Anthropic");
|
|
});
|
|
|
|
test("@google/generative-ai maps to ai-gateway", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["@google/generative-ai"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("ai-gateway");
|
|
expect(result.injected[0].message).toContain("Google");
|
|
});
|
|
|
|
test("@vercel/kv maps to vercel-storage with sunset message", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["@vercel/kv"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-storage");
|
|
expect(result.injected[0].message).toContain("sunset");
|
|
});
|
|
|
|
test("@sanity/client maps to cms", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["@sanity/client"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("cms");
|
|
});
|
|
|
|
test("contentful maps to cms", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["contentful"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("cms");
|
|
});
|
|
|
|
test("resend maps to email", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["resend"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("email");
|
|
});
|
|
|
|
test("fastify maps to vercel-functions", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["fastify"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-functions");
|
|
});
|
|
|
|
test("koa maps to vercel-functions", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["koa"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-functions");
|
|
});
|
|
|
|
test("bull maps to vercel-queues", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["bull"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-queues");
|
|
});
|
|
|
|
test("workflow maps to workflow", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["workflow"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("workflow");
|
|
});
|
|
|
|
test("ai maps to ai-sdk", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["ai"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("ai-sdk");
|
|
});
|
|
|
|
test("@ai-sdk/react maps to ai-sdk", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["@ai-sdk/react"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("ai-sdk");
|
|
});
|
|
|
|
test("@vercel/flags maps to vercel-flags", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["@vercel/flags"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-flags");
|
|
});
|
|
|
|
test("swr maps to swr", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["swr"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("swr");
|
|
});
|
|
|
|
test("node-cron maps to cron-jobs", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["node-cron"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("cron-jobs");
|
|
});
|
|
|
|
test("cron maps to cron-jobs", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["cron"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("cron-jobs");
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Iteration 2: new package entries
|
|
// -------------------------------------------------------------------
|
|
|
|
test("next-auth maps to auth", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["next-auth"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("auth");
|
|
expect(result.injected[0].message).toContain("next-auth");
|
|
});
|
|
|
|
test("@slack/bolt maps to chat-sdk", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["@slack/bolt"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("chat-sdk");
|
|
expect(result.injected[0].message).toContain("Chat SDK");
|
|
});
|
|
|
|
test("@slack/web-api maps to chat-sdk", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["@slack/web-api"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("chat-sdk");
|
|
});
|
|
|
|
test("discord.js maps to chat-sdk", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["discord.js"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("chat-sdk");
|
|
expect(result.injected[0].message).toContain("discord.js");
|
|
});
|
|
|
|
test("telegraf maps to chat-sdk", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["telegraf"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("chat-sdk");
|
|
expect(result.injected[0].message).toContain("Telegraf");
|
|
});
|
|
|
|
test("grammy maps to chat-sdk", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["grammy"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("chat-sdk");
|
|
expect(result.injected[0].message).toContain("Grammy");
|
|
});
|
|
|
|
test("helmet maps to vercel-firewall", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["helmet"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("vercel-firewall");
|
|
expect(result.injected[0].message).toContain("Firewall");
|
|
});
|
|
|
|
test("cors maps to routing-middleware", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["cors"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("routing-middleware");
|
|
expect(result.injected[0].message).toContain("Routing Middleware");
|
|
});
|
|
|
|
test("dotenv maps to env-vars", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "" };
|
|
const result = runBashChainInjection(["dotenv"], null, ROOT, undefined, cleanEnv);
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("env-vars");
|
|
expect(result.injected[0].message).toContain("vercel env");
|
|
});
|
|
|
|
// -------------------------------------------------------------------
|
|
// Regression: cross-package dedup and budget compliance
|
|
// -------------------------------------------------------------------
|
|
|
|
test("prisma + stripe + langchain install targets 3 distinct skills", () => {
|
|
const cleanEnv: any = { VERCEL_PLUGIN_SEEN_SKILLS: "", VERCEL_PLUGIN_CHAIN_CAP: "10" };
|
|
const result = runBashChainInjection(
|
|
["prisma", "stripe", "langchain"],
|
|
null, ROOT, undefined, cleanEnv,
|
|
);
|
|
expect(result.injected.length).toBe(3);
|
|
expect(result.injected[0].skill).toBe("vercel-storage");
|
|
expect(result.injected[1].skill).toBe("payments");
|
|
expect(result.injected[2].skill).toBe("ai-sdk");
|
|
expect(result.totalBytes).toBeLessThanOrEqual(18_000);
|
|
});
|
|
|
|
test("@clerk/nextjs + langchain + stripe dedup with seen skills", () => {
|
|
const envWithSeen: any = { VERCEL_PLUGIN_SEEN_SKILLS: "auth,ai-sdk" };
|
|
const result = runBashChainInjection(
|
|
["@clerk/nextjs", "langchain", "stripe"],
|
|
null, ROOT, undefined, envWithSeen,
|
|
);
|
|
// auth and ai-sdk already seen, only stripe/payments should inject
|
|
expect(result.injected.length).toBe(1);
|
|
expect(result.injected[0].skill).toBe("payments");
|
|
});
|
|
});
|
|
|
|
describe("posttooluse-bash-chain: formatBashChainOutput", () => {
|
|
let formatBashChainOutput: typeof import("../hooks/src/posttooluse-bash-chain.mts").formatBashChainOutput;
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import("../hooks/posttooluse-bash-chain.mjs");
|
|
formatBashChainOutput = mod.formatBashChainOutput;
|
|
});
|
|
|
|
test("empty injections return empty JSON", () => {
|
|
expect(formatBashChainOutput({ injected: [], totalBytes: 0 })).toBe("{}");
|
|
});
|
|
|
|
test("non-empty injections produce hookSpecificOutput with additionalContext", () => {
|
|
const result = formatBashChainOutput({
|
|
injected: [{
|
|
packageName: "express",
|
|
skill: "vercel-functions",
|
|
message: "Express.js detected",
|
|
content: "# Vercel Functions\nSome content here.",
|
|
}],
|
|
totalBytes: 100,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.hookSpecificOutput).toBeDefined();
|
|
expect(parsed.hookSpecificOutput.additionalContext).toContain("posttooluse-bash-chain:");
|
|
expect(parsed.hookSpecificOutput.additionalContext).toContain("vercel-functions");
|
|
expect(parsed.hookSpecificOutput.additionalContext).toContain("Express.js detected");
|
|
expect(parsed.hookSpecificOutput.additionalContext).toContain("postBashChain:");
|
|
});
|
|
});
|
|
|
|
describe("posttooluse-bash-chain: parseBashInput", () => {
|
|
let parseBashInput: typeof import("../hooks/src/posttooluse-bash-chain.mts").parseBashInput;
|
|
|
|
beforeEach(async () => {
|
|
const mod = await import("../hooks/posttooluse-bash-chain.mjs");
|
|
parseBashInput = mod.parseBashInput;
|
|
});
|
|
|
|
test("parses valid Bash tool input", () => {
|
|
const raw = JSON.stringify({
|
|
tool_name: "Bash",
|
|
tool_input: { command: "npm install express" },
|
|
session_id: "test-session",
|
|
});
|
|
const result = parseBashInput(raw);
|
|
expect(result).not.toBeNull();
|
|
expect(result!.command).toBe("npm install express");
|
|
expect(result!.sessionId).toBe("test-session");
|
|
});
|
|
|
|
test("returns null for non-Bash tool", () => {
|
|
const raw = JSON.stringify({
|
|
tool_name: "Write",
|
|
tool_input: { file_path: "/test.ts" },
|
|
});
|
|
expect(parseBashInput(raw)).toBeNull();
|
|
});
|
|
|
|
test("returns null for empty input", () => {
|
|
expect(parseBashInput("")).toBeNull();
|
|
});
|
|
|
|
test("returns null for invalid JSON", () => {
|
|
expect(parseBashInput("not json")).toBeNull();
|
|
});
|
|
|
|
test("returns null for Bash tool with no command", () => {
|
|
const raw = JSON.stringify({
|
|
tool_name: "Bash",
|
|
tool_input: {},
|
|
});
|
|
expect(parseBashInput(raw)).toBeNull();
|
|
});
|
|
});
|