refactor: remove paseo prompt instructions and add real agent tests

This commit is contained in:
Mohamed Boudra
2026-02-02 14:15:26 +07:00
parent a0214645eb
commit afce2e1c58
4 changed files with 132 additions and 334 deletions
@@ -1,321 +1,148 @@
import { describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { execSync } from "child_process";
import { mkdtempSync, rmSync, writeFileSync, realpathSync } from "fs";
import { tmpdir } from "os";
import path from "path";
import pino from "pino";
import { createTestLogger } from "../../test-utils/test-logger.js"; import { AgentManager } from "./agent-manager.js";
import type { AgentManager } from "./agent-manager.js"; import { AgentStorage } from "./agent-storage.js";
import { import { createAllClients, shutdownProviders } from "./provider-registry.js";
determineAgentMetadataNeeds, import { generateAndApplyAgentMetadata } from "./agent-metadata-generator.js";
generateAndApplyAgentMetadata, import { createWorktree, validateBranchSlug } from "../../utils/worktree.js";
scheduleAgentMetadataGeneration,
} from "./agent-metadata-generator.js";
const logger = createTestLogger(); const CODEX_TEST_MODEL = "gpt-5.1-codex-mini";
const CODEX_TEST_REASONING_EFFORT = "low";
function createAgentManagerStub() { function tmpCwd(prefix: string): string {
return { return realpathSync(mkdtempSync(path.join(tmpdir(), prefix)));
setTitle: vi.fn().mockResolvedValue(undefined),
} as unknown as AgentManager;
} }
function delayImmediate(): Promise<void> { function initGitRepo(repoDir: string): void {
return new Promise((resolve) => setImmediate(resolve)); execSync("git init -b main", { cwd: repoDir, stdio: "pipe" });
execSync("git config user.email 'paseo-test@example.com'", {
cwd: repoDir,
stdio: "pipe",
});
execSync("git config user.name 'Paseo Test'", {
cwd: repoDir,
stdio: "pipe",
});
writeFileSync(path.join(repoDir, "README.md"), "init\n");
execSync("git add README.md", { cwd: repoDir, stdio: "pipe" });
execSync("git -c commit.gpgsign=false commit -m 'Initial commit'", {
cwd: repoDir,
stdio: "pipe",
});
} }
function getSchemaKeys(schema: unknown): string[] { describe("agent metadata generation (real agents)", () => {
const shape = const logger = pino({ level: "silent" });
(schema as { shape?: Record<string, unknown> }).shape ?? let repoDir: string;
(schema as { _def?: { shape?: () => Record<string, unknown> } })._def?.shape?.(); let paseoHome: string;
return shape ? Object.keys(shape).sort() : []; let storagePath: string;
} let manager: AgentManager;
let storage: AgentStorage;
let codexSessionDir: string;
let previousCodexSessionDir: string | undefined;
describe("agent metadata generation", () => { beforeEach(() => {
it("skips generation when there is no initial prompt", async () => { repoDir = tmpCwd("metadata-repo-");
const agentManager = createAgentManagerStub(); initGitRepo(repoDir);
const generator = vi.fn(); paseoHome = tmpCwd("metadata-paseo-home-");
storagePath = path.join(paseoHome, "agents");
storage = new AgentStorage(storagePath, logger);
manager = new AgentManager({
clients: createAllClients(logger),
registry: storage,
logger,
});
codexSessionDir = tmpCwd("codex-sessions-");
previousCodexSessionDir = process.env.CODEX_SESSION_DIR;
process.env.CODEX_SESSION_DIR = codexSessionDir;
});
afterEach(async () => {
process.env.CODEX_SESSION_DIR = previousCodexSessionDir;
await shutdownProviders(logger);
rmSync(repoDir, { recursive: true, force: true });
rmSync(paseoHome, { recursive: true, force: true });
rmSync(codexSessionDir, { recursive: true, force: true });
}, 60000);
test(
"generates a title using a real Codex agent",
async () => {
const agent = await manager.createAgent({
provider: "codex",
model: CODEX_TEST_MODEL,
reasoningEffort: CODEX_TEST_REASONING_EFFORT,
modeId: "auto",
cwd: repoDir,
title: "Main Agent",
}, "metadata-title-agent");
await generateAndApplyAgentMetadata({ await generateAndApplyAgentMetadata({
agentManager, agentManager: manager,
agentId: "agent-1", agentId: agent.id,
cwd: "/tmp", cwd: repoDir,
initialPrompt: " ", initialPrompt: "Use the exact title 'Metadata Title E2E'.",
logger,
deps: { generateStructuredAgentResponse: generator },
});
expect(generator).not.toHaveBeenCalled();
});
it("determines title needs when explicit title is missing", async () => {
const needs = await determineAgentMetadataNeeds({
cwd: "/tmp",
initialPrompt: "Do the thing",
explicitTitle: null,
deps: {
getCheckoutStatus: async () => ({
isGit: false,
}),
},
});
expect(needs.needsTitle).toBe(true);
expect(needs.needsBranch).toBe(false);
});
it("schedules async title generation", async () => {
const agentManager = createAgentManagerStub();
const generator = vi.fn(async () => ({ title: "Generated Title" }));
scheduleAgentMetadataGeneration({
agentManager,
agentId: "agent-2",
cwd: "/tmp",
initialPrompt: "Create a report",
explicitTitle: null, explicitTitle: null,
paseoHome,
logger, logger,
deps: {
generateStructuredAgentResponse: generator,
getCheckoutStatus: async () => ({
isGit: false,
}),
},
}); });
expect(generator).not.toHaveBeenCalled(); await storage.flush();
const record = await storage.get(agent.id);
expect(record?.title).toBe("Metadata Title E2E");
await delayImmediate(); await manager.closeAgent(agent.id);
},
expect(generator).toHaveBeenCalledTimes(1); 180000
expect(agentManager.setTitle).toHaveBeenCalledWith(
"agent-2",
"Generated Title"
); );
test(
"renames the worktree branch using a real Codex agent",
async () => {
const worktreeSlug = "metadata-worktree";
const worktree = await createWorktree({
branchName: worktreeSlug,
cwd: repoDir,
baseBranch: "main",
worktreeSlug,
paseoHome,
}); });
it("selects title-only schema when branch is not eligible", async () => { const agent = await manager.createAgent({
const agentManager = createAgentManagerStub(); provider: "codex",
let capturedSchema: unknown; model: CODEX_TEST_MODEL,
const generator = vi.fn(async (options: any) => { reasoningEffort: CODEX_TEST_REASONING_EFFORT,
capturedSchema = options.schema; modeId: "auto",
return { title: "Only Title" }; cwd: worktree.worktreePath,
}); title: "Worktree Agent",
}, "metadata-branch-agent");
await generateAndApplyAgentMetadata({ await generateAndApplyAgentMetadata({
agentManager, agentManager: manager,
agentId: "agent-title-only", agentId: agent.id,
cwd: "/tmp", cwd: worktree.worktreePath,
initialPrompt: "Do the thing", initialPrompt: "Use the exact branch 'feat/metadata-worktree'.",
explicitTitle: null,
logger,
deps: {
generateStructuredAgentResponse: generator,
getCheckoutStatus: async () => ({
isGit: false,
}),
},
});
expect(getSchemaKeys(capturedSchema)).toEqual(["title"]);
});
it("selects branch-only schema when explicit title exists and branch is eligible", async () => {
const agentManager = createAgentManagerStub();
let capturedSchema: unknown;
const generator = vi.fn(async (options: any) => {
capturedSchema = options.schema;
return { branch: "feat/branch-only" };
});
const renameCurrentBranch = vi.fn().mockResolvedValue({
previousBranch: "worktree-name",
currentBranch: "feat/branch-only",
});
await generateAndApplyAgentMetadata({
agentManager,
agentId: "agent-branch-only",
cwd: "/tmp/worktree",
initialPrompt: "Add feature",
explicitTitle: "Explicit Title", explicitTitle: "Explicit Title",
paseoHome,
logger, logger,
deps: { });
generateStructuredAgentResponse: generator,
getCheckoutStatus: async () => ({ const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
isGit: true, cwd: worktree.worktreePath,
isPaseoOwnedWorktree: true, stdio: "pipe",
repoRoot: "/tmp/worktree-name", }).toString().trim();
mainRepoRoot: "/tmp/main",
currentBranch: "worktree-name", const validation = validateBranchSlug(currentBranch);
isDirty: false, expect(validation.valid).toBe(true);
baseRef: "main", expect(currentBranch).toBe("feat/metadata-worktree");
aheadBehind: null,
aheadOfOrigin: null, await manager.closeAgent(agent.id);
hasRemote: false,
remoteUrl: null,
}),
renameCurrentBranch,
}, },
}); 180000
expect(getSchemaKeys(capturedSchema)).toEqual(["branch"]);
expect(agentManager.setTitle).not.toHaveBeenCalled();
});
it("selects title+branch schema when both are needed", async () => {
const agentManager = createAgentManagerStub();
let capturedSchema: unknown;
const generator = vi.fn(async (options: any) => {
capturedSchema = options.schema;
return { title: "Both", branch: "feat/both" };
});
const renameCurrentBranch = vi.fn().mockResolvedValue({
previousBranch: "worktree-name",
currentBranch: "feat/both",
});
await generateAndApplyAgentMetadata({
agentManager,
agentId: "agent-both",
cwd: "/tmp/worktree",
initialPrompt: "Add payment support",
explicitTitle: null,
logger,
deps: {
generateStructuredAgentResponse: generator,
getCheckoutStatus: async () => ({
isGit: true,
isPaseoOwnedWorktree: true,
repoRoot: "/tmp/worktree-name",
mainRepoRoot: "/tmp/main",
currentBranch: "worktree-name",
isDirty: false,
baseRef: "main",
aheadBehind: null,
aheadOfOrigin: null,
hasRemote: false,
remoteUrl: null,
}),
renameCurrentBranch,
},
});
expect(getSchemaKeys(capturedSchema)).toEqual(["branch", "title"]);
});
it("renames branch when eligible, even with explicit title", async () => {
const agentManager = createAgentManagerStub();
const generator = vi.fn(async () => ({ branch: "feat/auto-branch" }));
const renameCurrentBranch = vi.fn().mockResolvedValue({
previousBranch: "worktree-name",
currentBranch: "feat/auto-branch",
});
await generateAndApplyAgentMetadata({
agentManager,
agentId: "agent-3",
cwd: "/tmp/worktree",
initialPrompt: "Add payment support",
explicitTitle: "Payments",
logger,
deps: {
generateStructuredAgentResponse: generator,
getCheckoutStatus: async () => ({
isGit: true,
isPaseoOwnedWorktree: true,
repoRoot: "/tmp/worktree-name",
mainRepoRoot: "/tmp/main",
currentBranch: "worktree-name",
isDirty: false,
baseRef: "main",
aheadBehind: null,
aheadOfOrigin: null,
hasRemote: false,
remoteUrl: null,
}),
renameCurrentBranch,
},
});
expect(generator).toHaveBeenCalledTimes(1);
expect(renameCurrentBranch).toHaveBeenCalledWith(
"/tmp/worktree",
"feat/auto-branch"
); );
}); });
it("skips rename when generated branch is invalid", async () => {
const agentManager = createAgentManagerStub();
const generator = vi.fn(async () => ({ branch: "Invalid Branch" }));
const renameCurrentBranch = vi.fn();
await generateAndApplyAgentMetadata({
agentManager,
agentId: "agent-invalid-branch",
cwd: "/tmp/worktree",
initialPrompt: "Ship it",
explicitTitle: "Explicit",
logger,
deps: {
generateStructuredAgentResponse: generator,
getCheckoutStatus: async () => ({
isGit: true,
isPaseoOwnedWorktree: true,
repoRoot: "/tmp/worktree-name",
mainRepoRoot: "/tmp/main",
currentBranch: "worktree-name",
isDirty: false,
baseRef: "main",
aheadBehind: null,
aheadOfOrigin: null,
hasRemote: false,
remoteUrl: null,
}),
renameCurrentBranch,
},
});
expect(renameCurrentBranch).not.toHaveBeenCalled();
});
it("schedules async branch generation for worktree agents", async () => {
const agentManager = createAgentManagerStub();
const generator = vi.fn(async () => ({ branch: "feat/async-branch" }));
const renameCurrentBranch = vi.fn().mockResolvedValue({
previousBranch: "worktree-name",
currentBranch: "feat/async-branch",
});
scheduleAgentMetadataGeneration({
agentManager,
agentId: "agent-4",
cwd: "/tmp/worktree",
initialPrompt: "Ship the feature",
explicitTitle: "Explicit Title",
logger,
deps: {
generateStructuredAgentResponse: generator,
getCheckoutStatus: async () => ({
isGit: true,
isPaseoOwnedWorktree: true,
repoRoot: "/tmp/worktree-name",
mainRepoRoot: "/tmp/main",
currentBranch: "worktree-name",
isDirty: false,
baseRef: "main",
aheadBehind: null,
aheadOfOrigin: null,
hasRemote: false,
remoteUrl: null,
}),
renameCurrentBranch,
},
});
expect(generator).not.toHaveBeenCalled();
await delayImmediate();
expect(generator).toHaveBeenCalledTimes(1);
expect(renameCurrentBranch).toHaveBeenCalledWith(
"/tmp/worktree",
"feat/async-branch"
);
});
});
@@ -14,7 +14,9 @@ import {
type CheckoutStatusResult, type CheckoutStatusResult,
} from "../../utils/checkout-git.js"; } from "../../utils/checkout-git.js";
const AUTO_GEN_MODEL = "haiku"; const AUTO_GEN_PROVIDER = "codex" as const;
const AUTO_GEN_MODEL = "gpt-5.1-codex-mini";
const AUTO_GEN_REASONING_EFFORT = "low";
export type AgentMetadataGeneratorDeps = { export type AgentMetadataGeneratorDeps = {
generateStructuredAgentResponse?: typeof generateStructuredAgentResponse; generateStructuredAgentResponse?: typeof generateStructuredAgentResponse;
@@ -156,8 +158,9 @@ export async function generateAndApplyAgentMetadata(
result = await generator({ result = await generator({
manager: options.agentManager, manager: options.agentManager,
agentConfig: { agentConfig: {
provider: "claude", provider: AUTO_GEN_PROVIDER,
model: AUTO_GEN_MODEL, model: AUTO_GEN_MODEL,
reasoningEffort: AUTO_GEN_REASONING_EFFORT,
cwd: options.cwd, cwd: options.cwd,
title: "Agent metadata generator", title: "Agent metadata generator",
internal: true, internal: true,
@@ -147,8 +147,6 @@ describe("Codex app-server provider (integration)", () => {
cwd, cwd,
modeId: "auto", modeId: "auto",
approvalPolicy: "on-request", approvalPolicy: "on-request",
paseoPromptInstructions:
"You must use the shell tool for command execution tasks. Do not answer without running the command.",
model: CODEX_TEST_MODEL, model: CODEX_TEST_MODEL,
reasoningEffort: CODEX_TEST_REASONING_EFFORT, reasoningEffort: CODEX_TEST_REASONING_EFFORT,
}); });
@@ -229,8 +227,6 @@ describe("Codex app-server provider (integration)", () => {
cwd, cwd,
modeId: "full-access", modeId: "full-access",
approvalPolicy: "on-request", approvalPolicy: "on-request",
paseoPromptInstructions:
"You must use shell for commands and apply_patch for file edits. Do not skip tool usage.",
model: CODEX_TEST_MODEL, model: CODEX_TEST_MODEL,
reasoningEffort: CODEX_TEST_REASONING_EFFORT, reasoningEffort: CODEX_TEST_REASONING_EFFORT,
}); });
@@ -552,8 +548,6 @@ describe("Codex app-server provider (integration)", () => {
cwd, cwd,
modeId: "full-access", modeId: "full-access",
approvalPolicy: "on-request", approvalPolicy: "on-request",
paseoPromptInstructions:
"You must use the apply_patch tool for file edits. Do not use the shell tool for file changes.",
model: CODEX_TEST_MODEL, model: CODEX_TEST_MODEL,
reasoningEffort: CODEX_TEST_REASONING_EFFORT, reasoningEffort: CODEX_TEST_REASONING_EFFORT,
}); });
@@ -616,9 +610,6 @@ describe("Codex app-server provider (integration)", () => {
expect(sawPermissionResolved).toBe(true); expect(sawPermissionResolved).toBe(true);
} }
expect(sawPermission || timelineItems.length > 0).toBe(true); expect(sawPermission || timelineItems.length > 0).toBe(true);
expect(
timelineItems.some((item) => item.type === "tool_call" && item.name === "apply_patch")
).toBe(true);
expect(readFileSync(targetPath, "utf8").trim()).toBe("ok"); expect(readFileSync(targetPath, "utf8").trim()).toBe("ok");
} finally { } finally {
cleanup(); cleanup();
@@ -33,7 +33,6 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import readline from "node:readline"; import readline from "node:readline";
import { injectLeadingPaseoInstructionTag } from "../paseo-instructions-tag.js";
const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000; const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000;
const CODEX_PROVIDER = "codex" as const; const CODEX_PROVIDER = "codex" as const;
@@ -1335,38 +1334,16 @@ class CodexAppServerAgentSession implements AgentSession {
private buildUserInput(prompt: AgentPromptInput): unknown[] { private buildUserInput(prompt: AgentPromptInput): unknown[] {
if (typeof prompt === "string") { if (typeof prompt === "string") {
const text = this.paseoInstructionsInjected
? prompt
: injectLeadingPaseoInstructionTag(prompt, this.config.paseoPromptInstructions);
this.paseoInstructionsInjected = true; this.paseoInstructionsInjected = true;
return [{ type: "text", text }]; return [{ type: "text", text: prompt }];
} }
const blocks = prompt as AgentPromptContentBlock[]; const blocks = prompt as AgentPromptContentBlock[];
if (this.paseoInstructionsInjected) { if (this.paseoInstructionsInjected) {
return blocks; return blocks;
} }
this.paseoInstructionsInjected = true; this.paseoInstructionsInjected = true;
if (blocks.length === 0) {
return blocks; return blocks;
} }
const first = blocks[0];
if (first && typeof first === "object" && (first as { type?: string }).type === "text") {
const textBlock = first as { type: "text"; text: string };
const text = injectLeadingPaseoInstructionTag(
textBlock.text ?? "",
this.config.paseoPromptInstructions
);
return [{ ...textBlock, text }, ...blocks.slice(1)];
}
const injected = injectLeadingPaseoInstructionTag(
"",
this.config.paseoPromptInstructions
);
if (injected.trim().length === 0) {
return blocks;
}
return [{ type: "text", text: injected }, ...blocks];
}
private emitEvent(event: AgentStreamEvent): void { private emitEvent(event: AgentStreamEvent): void {
if (event.type === "timeline") { if (event.type === "timeline") {