Files
vercel__vercel-plugin/tests/subagent-start-context.test.ts
John Lindquist 1e0fc2467c feat(hooks): wire subagent launch bootstrap
Connect SubagentStart to pending launch records so prompt-signal matches
can be merged into likely skills before context assembly.
SessionEnd now cleans hashed pending-launch temp directories and the
hook registry observes Agent tool launches.

Verified: node --input-type=module -e "import { build } from 'tsup'; await build({ entry: ['hooks/src/subagent-start-bootstrap.mts','hooks/src/session-end-cleanup.mts'], format: ['esm'], outDir: 'hooks', outExtension: () => ({ js: '.mjs' }), bundle: false, splitting: false, sourcemap: false, dts: false, clean: false, target: 'node20' });"
Verified: bun test tests/subagent-start-context.test.ts tests/session-end-cleanup.test.ts
Swarm-Agent: codex-wiring
2026-03-09 16:30:02 -06:00

333 lines
9.6 KiB
TypeScript

import { describe, test, expect, beforeEach } from "bun:test";
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { appendPendingLaunch, type PendingLaunch } from "../hooks/src/subagent-state.mts";
const ROOT = resolve(import.meta.dirname, "..");
const HOOK_SCRIPT = join(ROOT, "hooks", "subagent-start-bootstrap.mjs");
let testSession: string;
let tempDir: string;
beforeEach(() => {
testSession = `subagent-ctx-${Date.now()}-${Math.random().toString(36).slice(2)}`;
tempDir = mkdtempSync(join(tmpdir(), "subagent-ctx-"));
});
/**
* Run the SubagentStart bootstrap hook by piping JSON on stdin.
*/
async function runSubagentStart(
input: {
session_id?: string;
agent_id?: string;
agent_type?: string;
cwd?: string;
},
env?: Record<string, string | undefined>,
): Promise<{ code: number; stdout: string; stderr: string }> {
const payload = JSON.stringify({
session_id: testSession,
hook_event_name: "SubagentStart",
...input,
});
const proc = Bun.spawn(["node", HOOK_SCRIPT], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
VERCEL_PLUGIN_LOG_LEVEL: "off",
...env,
},
});
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();
return { code, stdout, stderr };
}
function parseContext(stdout: string): string {
if (!stdout.trim()) return "";
const parsed = JSON.parse(stdout);
return parsed?.hookSpecificOutput?.additionalContext || "";
}
/**
* Write a fake profile cache to disk so the hook can read it.
*/
function writeProfileCache(sessionId: string, likelySkills: string[]): void {
// Profile cache path follows: <tmpdir>/vercel-plugin-<sessionId>-profile.json
const cachePath = join(tmpdir(), `vercel-plugin-${sessionId}-profile.json`);
writeFileSync(
cachePath,
JSON.stringify({
projectRoot: "/Users/me/project",
likelySkills,
greenfield: false,
bootstrapHints: [],
resourceHints: [],
setupMode: false,
agentBrowserAvailable: false,
timestamp: new Date().toISOString(),
}),
"utf-8",
);
}
function cleanupProfileCache(sessionId: string): void {
const cachePath = join(tmpdir(), `vercel-plugin-${sessionId}-profile.json`);
try {
rmSync(cachePath, { force: true });
} catch {}
}
function cleanupPendingLaunches(sessionId: string): void {
const pendingLaunchPath = join(tmpdir(), `vercel-plugin-${sessionId}-pending-launches.jsonl`);
try {
rmSync(pendingLaunchPath, { force: true });
rmSync(`${pendingLaunchPath}.lock`, { force: true });
} catch {}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("subagent-start-context: additionalContext per agent type", () => {
test("Explore agent receives minimal budget context", async () => {
writeProfileCache(testSession, ["nextjs", "vercel-storage"]);
try {
const { code, stdout } = await runSubagentStart({
agent_id: "explore-1",
agent_type: "Explore",
});
expect(code).toBe(0);
const ctx = parseContext(stdout);
expect(ctx).toContain('budget="minimal"');
expect(ctx).toContain('agent_type="Explore"');
expect(ctx).toContain("nextjs");
expect(ctx).toContain("vercel-storage");
} finally {
cleanupProfileCache(testSession);
}
});
test("Plan agent receives light budget context with summaries", async () => {
writeProfileCache(testSession, ["nextjs", "vercel-storage"]);
try {
const { code, stdout } = await runSubagentStart({
agent_id: "plan-1",
agent_type: "Plan",
});
expect(code).toBe(0);
const ctx = parseContext(stdout);
expect(ctx).toContain('budget="light"');
expect(ctx).toContain('agent_type="Plan"');
expect(ctx).toContain("nextjs");
} finally {
cleanupProfileCache(testSession);
}
});
test("general-purpose agent receives standard budget context", async () => {
writeProfileCache(testSession, ["nextjs"]);
try {
const { code, stdout } = await runSubagentStart({
agent_id: "gp-1",
agent_type: "general-purpose",
});
expect(code).toBe(0);
const ctx = parseContext(stdout);
expect(ctx).toContain('budget="standard"');
expect(ctx).toContain('agent_type="general-purpose"');
expect(ctx).toContain("nextjs");
} finally {
cleanupProfileCache(testSession);
}
});
test("unknown agent type falls back to standard budget", async () => {
writeProfileCache(testSession, ["nextjs"]);
try {
const { code, stdout } = await runSubagentStart({
agent_id: "custom-1",
agent_type: "my-custom-agent",
});
expect(code).toBe(0);
const ctx = parseContext(stdout);
expect(ctx).toContain('budget="standard"');
expect(ctx).toContain('agent_type="my-custom-agent"');
} finally {
cleanupProfileCache(testSession);
}
});
});
describe("subagent-start-context: budget enforcement", () => {
test("Explore context stays within 1KB budget", async () => {
// Give it many skills to potentially exceed budget
writeProfileCache(testSession, [
"nextjs", "vercel-storage", "ai-sdk", "shadcn", "auth",
"vercel-functions", "edge-runtime", "turborepo",
]);
try {
const { code, stdout } = await runSubagentStart({
agent_id: "explore-budget",
agent_type: "Explore",
});
expect(code).toBe(0);
const ctx = parseContext(stdout);
expect(Buffer.byteLength(ctx, "utf8")).toBeLessThanOrEqual(1024);
} finally {
cleanupProfileCache(testSession);
}
});
test("Plan context stays within 3KB budget", async () => {
writeProfileCache(testSession, [
"nextjs", "vercel-storage", "ai-sdk", "shadcn", "auth",
"vercel-functions", "edge-runtime", "turborepo",
]);
try {
const { code, stdout } = await runSubagentStart({
agent_id: "plan-budget",
agent_type: "Plan",
});
expect(code).toBe(0);
const ctx = parseContext(stdout);
expect(Buffer.byteLength(ctx, "utf8")).toBeLessThanOrEqual(3072);
} finally {
cleanupProfileCache(testSession);
}
});
test("general-purpose context stays within 8KB budget", async () => {
writeProfileCache(testSession, [
"nextjs", "vercel-storage", "ai-sdk", "shadcn", "auth",
"vercel-functions", "edge-runtime", "turborepo",
]);
try {
const { code, stdout } = await runSubagentStart({
agent_id: "gp-budget",
agent_type: "general-purpose",
});
expect(code).toBe(0);
const ctx = parseContext(stdout);
expect(Buffer.byteLength(ctx, "utf8")).toBeLessThanOrEqual(8000);
} finally {
cleanupProfileCache(testSession);
}
});
});
describe("subagent-start-context: profile cache and fallback", () => {
test("falls back to VERCEL_PLUGIN_LIKELY_SKILLS env when no profile cache", async () => {
// No profile cache written — should fall back to env var
const { code, stdout } = await runSubagentStart(
{
agent_id: "fallback-1",
agent_type: "general-purpose",
},
{
VERCEL_PLUGIN_LIKELY_SKILLS: "nextjs,ai-sdk",
},
);
expect(code).toBe(0);
const ctx = parseContext(stdout);
expect(ctx).toContain("nextjs");
expect(ctx).toContain("ai-sdk");
});
test("returns context even with no likely skills", async () => {
const { code, stdout } = await runSubagentStart(
{
agent_id: "empty-1",
agent_type: "Explore",
},
{
VERCEL_PLUGIN_LIKELY_SKILLS: "",
},
);
expect(code).toBe(0);
const ctx = parseContext(stdout);
expect(ctx).toContain("Vercel plugin active");
expect(ctx).toContain("unknown stack");
});
test("merges pending launch prompt matches into likely skills before context assembly", async () => {
writeProfileCache(testSession, ["nextjs"]);
const pendingLaunch: PendingLaunch = {
description: "Plan a durable workflow",
prompt: "Use Workflow DevKit retries and step orchestration for this task",
subagent_type: "Plan",
createdAt: Date.now(),
};
appendPendingLaunch(testSession, pendingLaunch);
try {
const firstRun = await runSubagentStart({
agent_id: "plan-prompt",
agent_type: "Plan",
});
expect(firstRun.code).toBe(0);
const firstContext = parseContext(firstRun.stdout);
expect(firstContext).toContain("Project likely uses: workflow, nextjs.");
const secondRun = await runSubagentStart({
agent_id: "plan-prompt-2",
agent_type: "Plan",
});
expect(secondRun.code).toBe(0);
const secondContext = parseContext(secondRun.stdout);
expect(secondContext).toContain("Project likely uses: nextjs.");
} finally {
cleanupProfileCache(testSession);
cleanupPendingLaunches(testSession);
}
});
test("empty stdin produces empty output (no crash)", async () => {
const proc = Bun.spawn(["node", HOOK_SCRIPT], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: { ...process.env, VERCEL_PLUGIN_LOG_LEVEL: "off" },
});
proc.stdin.write("");
proc.stdin.end();
const code = await proc.exited;
const stdout = await new Response(proc.stdout).text();
expect(code).toBe(0);
expect(stdout.trim()).toBe("");
});
});