mirror of
https://github.com/vercel/vercel-plugin.git
synced 2026-09-14 15:39:47 +08:00
8e3350454c
- 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
215 lines
7.9 KiB
TypeScript
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":[]');
|
|
});
|
|
});
|