mirror of
https://github.com/vercel/vercel-plugin.git
synced 2026-09-14 15:39:47 +08:00
264 lines
8.6 KiB
TypeScript
264 lines
8.6 KiB
TypeScript
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
import { resolve } from "node:path";
|
|
|
|
const SCRIPT = resolve(import.meta.dir, "../scripts/prompt-signals-explain.ts");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface RunResult {
|
|
exitCode: number;
|
|
stdout: string;
|
|
stderr: string;
|
|
}
|
|
|
|
async function run(args: string[], stdin?: string): Promise<RunResult> {
|
|
const proc = Bun.spawn(["bun", "run", SCRIPT, ...args], {
|
|
stdin: stdin !== undefined ? new Blob([stdin]) : undefined,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: {
|
|
...process.env,
|
|
// Ensure dedup env var is set (matches production hook behavior)
|
|
VERCEL_PLUGIN_SEEN_SKILLS: "",
|
|
},
|
|
});
|
|
const [stdout, stderr] = await Promise.all([
|
|
new Response(proc.stdout).text(),
|
|
new Response(proc.stderr).text(),
|
|
]);
|
|
const exitCode = await proc.exited;
|
|
return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Environment cleanup
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let savedDedup: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
savedDedup = process.env.VERCEL_PLUGIN_HOOK_DEDUP;
|
|
delete process.env.VERCEL_PLUGIN_HOOK_DEDUP;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (savedDedup !== undefined) {
|
|
process.env.VERCEL_PLUGIN_HOOK_DEDUP = savedDedup;
|
|
} else {
|
|
delete process.env.VERCEL_PLUGIN_HOOK_DEDUP;
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Human output format
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("human output", () => {
|
|
test("--prompt shows table with ai-elements matched", async () => {
|
|
const result = await run([
|
|
"--prompt",
|
|
"add markdown formatting to streamed text",
|
|
]);
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.stdout).toContain("Prompt:");
|
|
expect(result.stdout).toContain("ai-elements");
|
|
expect(result.stdout).toContain("Score");
|
|
expect(result.stdout).toContain("Selected:");
|
|
});
|
|
|
|
test("no-match prompt still exits 0", async () => {
|
|
const result = await run([
|
|
"--prompt",
|
|
"refactor the database connection pool layer",
|
|
]);
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.stdout).toContain("Prompt:");
|
|
// No "Selected:" line when nothing matches
|
|
expect(result.stdout).not.toContain("Selected:");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// JSON output shape
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("JSON output", () => {
|
|
test("--json outputs valid PromptAnalysisReport", async () => {
|
|
const result = await run([
|
|
"--prompt",
|
|
"add markdown formatting to streamed text",
|
|
"--json",
|
|
]);
|
|
expect(result.exitCode).toBe(0);
|
|
|
|
const report = JSON.parse(result.stdout);
|
|
expect(report).toHaveProperty("normalizedPrompt");
|
|
expect(report).toHaveProperty("perSkillResults");
|
|
expect(report).toHaveProperty("selectedSkills");
|
|
expect(report).toHaveProperty("droppedByCap");
|
|
expect(report).toHaveProperty("droppedByBudget");
|
|
expect(report).toHaveProperty("dedupState");
|
|
expect(report).toHaveProperty("budgetBytes");
|
|
expect(report).toHaveProperty("timingMs");
|
|
expect(report.dedupState).toHaveProperty("strategy");
|
|
expect(report.dedupState).toHaveProperty("seenSkills");
|
|
expect(report.dedupState).toHaveProperty("filteredByDedup");
|
|
|
|
// ai-elements should match this prompt
|
|
expect(report.selectedSkills).toContain("ai-elements");
|
|
expect(report.perSkillResults["ai-elements"]?.matched).toBe(true);
|
|
});
|
|
|
|
test("no-match JSON has empty selectedSkills", async () => {
|
|
const result = await run([
|
|
"--prompt",
|
|
"refactor the database connection pool layer",
|
|
"--json",
|
|
]);
|
|
expect(result.exitCode).toBe(0);
|
|
|
|
const report = JSON.parse(result.stdout);
|
|
expect(report.selectedSkills).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Seen-skills dedup
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("--seen-skills dedup", () => {
|
|
test("seen skills are excluded from selection", async () => {
|
|
const result = await run([
|
|
"--prompt",
|
|
"add markdown formatting to streamed text",
|
|
"--json",
|
|
"--seen-skills",
|
|
"ai-elements",
|
|
]);
|
|
expect(result.exitCode).toBe(0);
|
|
|
|
const report = JSON.parse(result.stdout);
|
|
expect(report.selectedSkills).not.toContain("ai-elements");
|
|
expect(report.dedupState.filteredByDedup).toContain("ai-elements");
|
|
expect(report.dedupState.seenSkills).toContain("ai-elements");
|
|
});
|
|
|
|
test("multiple seen skills excluded", async () => {
|
|
const result = await run([
|
|
"--prompt",
|
|
"use next.js app router with ai sdk streamtext",
|
|
"--json",
|
|
"--seen-skills",
|
|
"nextjs,ai-sdk",
|
|
]);
|
|
expect(result.exitCode).toBe(0);
|
|
|
|
const report = JSON.parse(result.stdout);
|
|
expect(report.selectedSkills).not.toContain("nextjs");
|
|
expect(report.selectedSkills).not.toContain("ai-sdk");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Budget cap
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("--budget-bytes", () => {
|
|
test("tiny budget limits selection", async () => {
|
|
const result = await run([
|
|
"--prompt",
|
|
"use ai sdk streamtext and ai elements streaming markdown in terminal",
|
|
"--json",
|
|
"--budget-bytes",
|
|
"500",
|
|
"--max-skills",
|
|
"10",
|
|
]);
|
|
expect(result.exitCode).toBe(0);
|
|
|
|
const report = JSON.parse(result.stdout);
|
|
// With 500 byte budget, not all matched skills can be selected
|
|
const totalMatched = Object.values(
|
|
report.perSkillResults as Record<string, { matched: boolean }>,
|
|
).filter((r) => r.matched).length;
|
|
|
|
if (totalMatched > 1) {
|
|
// At least one should be budget-dropped or the budget was big enough for all
|
|
const totalSelected =
|
|
report.selectedSkills.length + report.droppedByBudget.length;
|
|
expect(totalSelected).toBeGreaterThanOrEqual(1);
|
|
}
|
|
expect(report.budgetBytes).toBe(500);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// --max-skills cap
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("--max-skills", () => {
|
|
test("max-skills=1 selects at most one skill", async () => {
|
|
const result = await run([
|
|
"--prompt",
|
|
"use ai sdk streamtext and ai elements streaming markdown in terminal",
|
|
"--json",
|
|
"--max-skills",
|
|
"1",
|
|
]);
|
|
expect(result.exitCode).toBe(0);
|
|
|
|
const report = JSON.parse(result.stdout);
|
|
expect(report.selectedSkills.length).toBeLessThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Stdin pipe
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("stdin mode", () => {
|
|
test("reads prompt from stdin", async () => {
|
|
const result = await run(["--json"], "stream markdown in terminal");
|
|
expect(result.exitCode).toBe(0);
|
|
|
|
const report = JSON.parse(result.stdout);
|
|
expect(report).toHaveProperty("normalizedPrompt");
|
|
expect(report.normalizedPrompt).toBe("stream markdown in terminal");
|
|
});
|
|
|
|
test("stdin with human output", async () => {
|
|
const result = await run([], "add streaming markdown to chat ui");
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.stdout).toContain("Prompt:");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Empty prompt
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("empty prompt", () => {
|
|
test("no prompt and no stdin exits with usage message", async () => {
|
|
// TTY detection: when stdin is a pipe but empty, it should error
|
|
const result = await run([], "");
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.stderr).toContain("no prompt");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Help
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("help", () => {
|
|
test("--help exits 0 with usage info", async () => {
|
|
const result = await run(["--help"]);
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.stdout).toContain("Usage:");
|
|
expect(result.stdout).toContain("--prompt");
|
|
expect(result.stdout).toContain("--json");
|
|
});
|
|
});
|