Files
vercel__vercel-plugin/hooks/user-prompt-submit-skill-inject.test.ts
John Lindquist 8e3350454c feat: prompt scoring improvements, compaction re-injection, validation dedup, strip droppedByCap from context; bump to 0.18.0
- Add dominant-topic suppression (top score >= 600 filters scores < 50)
- Add project-context boost (+3 for VERCEL_PLUGIN_LIKELY_SKILLS skills)
- Add lexical fallback floor (reject raw score < 20)
- Add compaction re-injection for priority >= 7 skills (VERCEL_PLUGIN_CONTEXT_COMPACTED)
- Add validation rule dedup tracking (ruleId + filePath on violations)
- Strip droppedByCap from injected HTML comments (kept in debug logs)
- Fix ncc/SKILL.md and next-forge/SKILL.md YAML frontmatter
- Add startup diagnostic for broken skill frontmatter
2026-03-11 19:01:59 -06:00

215 lines
7.9 KiB
TypeScript

import { afterEach, describe, expect, it } from "bun:test";
import { rmSync } from "node:fs";
import {
dedupClaimDirPath,
dedupFilePath,
listSessionKeys,
readSessionFile,
tryClaimSessionKey,
writeSessionFile,
} from "./src/hook-env.mts";
import { formatOutput as formatPreToolOutput } from "./src/pretooluse-skill-inject.mts";
import { mergeSeenSkillStatesWithCompactionReset } from "./src/patterns.mts";
import {
formatOutput,
parsePromptInput,
resolvePromptSeenSkillState,
syncPromptSeenSkillClaims,
} from "./src/user-prompt-submit-skill-inject.mts";
const SESSION_KIND = "seen-skills";
const originalSeenSkills = process.env.VERCEL_PLUGIN_SEEN_SKILLS;
const originalContextCompacted = process.env.VERCEL_PLUGIN_CONTEXT_COMPACTED;
const originalDedupMode = process.env.VERCEL_PLUGIN_HOOK_DEDUP;
const touchedSessionIds = new Set<string>();
function restoreEnv(
name: "VERCEL_PLUGIN_SEEN_SKILLS" | "VERCEL_PLUGIN_CONTEXT_COMPACTED" | "VERCEL_PLUGIN_HOOK_DEDUP",
value: string | undefined,
): void {
if (typeof value === "string") {
process.env[name] = value;
return;
}
delete process.env[name];
}
function cleanupSession(sessionId: string): void {
rmSync(dedupFilePath(sessionId, SESSION_KIND), { force: true });
rmSync(dedupClaimDirPath(sessionId, SESSION_KIND), { recursive: true, force: true });
}
function newSessionId(name: string): string {
const sessionId = `user-prompt-submit-${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
touchedSessionIds.add(sessionId);
return sessionId;
}
afterEach(() => {
restoreEnv("VERCEL_PLUGIN_SEEN_SKILLS", originalSeenSkills);
restoreEnv("VERCEL_PLUGIN_CONTEXT_COMPACTED", originalContextCompacted);
restoreEnv("VERCEL_PLUGIN_HOOK_DEDUP", originalDedupMode);
for (const sessionId of touchedSessionIds) {
cleanupSession(sessionId);
}
touchedSessionIds.clear();
});
describe("user prompt seen-skills dedup state", () => {
it("test_resolvePromptSeenSkillState_merges_file_and_claims_when_session_id_present", () => {
const sessionId = newSessionId("merge");
process.env.VERCEL_PLUGIN_HOOK_DEDUP = "";
process.env.VERCEL_PLUGIN_SEEN_SKILLS = "skill-env,shared";
writeSessionFile(sessionId, SESSION_KIND, "skill-file,shared");
tryClaimSessionKey(sessionId, SESSION_KIND, "skill-claim");
const state = resolvePromptSeenSkillState(sessionId);
expect(state.dedupOff).toBe(false);
expect(state.hasFileDedup).toBe(true);
expect("hasEnvDedup" in state).toBe(false);
expect("seenEnv" in state).toBe(false);
expect(state.seenClaims).toBe("skill-claim");
expect(state.seenState).toBe("shared,skill-claim,skill-file");
expect(readSessionFile(sessionId, SESSION_KIND)).toBe(state.seenState);
expect(process.env.VERCEL_PLUGIN_SEEN_SKILLS).toBe("skill-env,shared");
});
it("test_syncPromptSeenSkillClaims_updates_claims_and_session_file_when_skills_injected", () => {
const sessionId = newSessionId("sync");
process.env.VERCEL_PLUGIN_SEEN_SKILLS = "skill-env";
const synced = syncPromptSeenSkillClaims(sessionId, ["skill-new", "skill-env"]);
expect(synced).toBe("skill-env,skill-new");
expect(readSessionFile(sessionId, SESSION_KIND)).toBe("skill-env,skill-new");
expect(process.env.VERCEL_PLUGIN_SEEN_SKILLS).toBe("skill-env");
});
it("test_mergeSeenSkillStatesWithCompactionReset_clears_only_high_priority_skills_across_session_artifacts", () => {
const sessionId = newSessionId("compact-reset");
const scopeId = "subagent-compact-reset";
process.env.VERCEL_PLUGIN_SEEN_SKILLS = "ai-sdk,low-skill,shared";
process.env.VERCEL_PLUGIN_CONTEXT_COMPACTED = "true";
writeSessionFile(sessionId, SESSION_KIND, "ai-sdk,low-skill");
writeSessionFile(sessionId, SESSION_KIND, "low-skill,workflow", scopeId);
tryClaimSessionKey(sessionId, SESSION_KIND, "ai-sdk");
tryClaimSessionKey(sessionId, SESSION_KIND, "low-skill");
tryClaimSessionKey(sessionId, SESSION_KIND, "workflow", scopeId);
tryClaimSessionKey(sessionId, SESSION_KIND, "low-skill", scopeId);
const result = mergeSeenSkillStatesWithCompactionReset(
process.env.VERCEL_PLUGIN_SEEN_SKILLS,
readSessionFile(sessionId, SESSION_KIND),
listSessionKeys(sessionId, SESSION_KIND).join(","),
{
sessionId,
includeEnv: false,
skillMap: {
"ai-sdk": { priority: 7 },
workflow: { priority: 8 },
"low-skill": { priority: 6 },
},
},
);
expect(result.compactionResetApplied).toBe(true);
expect(result.clearedSkills).toEqual(["ai-sdk", "workflow"]);
expect(result.seenState).toBe("low-skill");
expect(process.env.VERCEL_PLUGIN_SEEN_SKILLS).toBe("low-skill,shared");
expect(process.env.VERCEL_PLUGIN_CONTEXT_COMPACTED).toBe("");
expect(readSessionFile(sessionId, SESSION_KIND)).toBe("low-skill");
expect(readSessionFile(sessionId, SESSION_KIND, scopeId)).toBe("low-skill");
expect(listSessionKeys(sessionId, SESSION_KIND)).toEqual(["low-skill"]);
expect(listSessionKeys(sessionId, SESSION_KIND, scopeId)).toEqual(["low-skill"]);
expect(tryClaimSessionKey(sessionId, SESSION_KIND, "ai-sdk")).toBe(true);
expect(tryClaimSessionKey(sessionId, SESSION_KIND, "workflow", scopeId)).toBe(true);
});
});
describe("user prompt cursor compatibility", () => {
it("test_parsePromptInput_normalizes_cursor_fields_when_workspace_root_present", () => {
const parsed = parsePromptInput(
JSON.stringify({
conversation_id: "cursor-conversation",
workspace_roots: ["/tmp/cursor-workspace", "/tmp/ignored"],
cursor_version: "1.0.0",
prompt: "Use ai elements for streaming markdown in this chat UI",
}),
);
expect(parsed).toEqual({
prompt: "Use ai elements for streaming markdown in this chat UI",
platform: "cursor",
sessionId: "cursor-conversation",
cwd: "/tmp/cursor-workspace",
});
});
it("test_parsePromptInput_uses_cursor_message_and_project_dir_fallbacks", () => {
const parsed = parsePromptInput(
JSON.stringify({
conversation_id: "cursor-conversation",
cursor_version: "1.0.0",
message: "Use ai elements for streaming markdown in this chat UI",
}),
undefined,
{
...process.env,
CURSOR_PROJECT_DIR: "/tmp/cursor-project",
CLAUDE_PROJECT_ROOT: "/tmp/claude-project",
},
);
expect(parsed).toEqual({
prompt: "Use ai elements for streaming markdown in this chat UI",
platform: "cursor",
sessionId: "cursor-conversation",
cwd: "/tmp/cursor-project",
});
});
it("test_formatOutput_returns_cursor_flat_shape_with_continue_and_env", () => {
const output = JSON.parse(formatOutput(
["You must run the Skill(ai-elements) tool."],
["ai-elements"],
["ai-elements"],
[],
[],
[],
{ "ai-elements": "matched streaming markdown" },
undefined,
"cursor",
{ VERCEL_PLUGIN_SEEN_SKILLS: "ai-elements" },
));
expect(output.continue).toBe(true);
expect(output.additional_context).toContain("Skill(ai-elements)");
expect(output.additional_context).toContain("skillInjection");
expect(output.env).toEqual({ VERCEL_PLUGIN_SEEN_SKILLS: "ai-elements" });
expect(output.hookSpecificOutput).toBeUndefined();
});
it("test_formatOutput_omits_droppedByCap_from_prettooluse_injected_metadata_comment", () => {
const output = JSON.parse(formatPreToolOutput({
parts: ["You must run the Skill(ai-sdk) tool."],
matched: new Set(["ai-sdk", "workflow"]),
injectedSkills: ["ai-sdk"],
summaryOnly: [],
droppedByCap: ["workflow"],
droppedByBudget: [],
toolName: "Write",
toolTarget: "app/page.tsx",
platform: "claude-code",
}));
const context = output.hookSpecificOutput?.additionalContext ?? "";
expect(context).toContain("skillInjection");
expect(context).not.toContain('"droppedByCap"');
expect(context).toContain('"droppedByBudget":[]');
});
});