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");
await generateAndApplyAgentMetadata({ storage = new AgentStorage(storagePath, logger);
agentManager, manager = new AgentManager({
agentId: "agent-1", clients: createAllClients(logger),
cwd: "/tmp", registry: storage,
initialPrompt: " ",
logger, logger,
deps: { generateStructuredAgentResponse: generator },
}); });
codexSessionDir = tmpCwd("codex-sessions-");
expect(generator).not.toHaveBeenCalled(); previousCodexSessionDir = process.env.CODEX_SESSION_DIR;
process.env.CODEX_SESSION_DIR = codexSessionDir;
}); });
it("determines title needs when explicit title is missing", async () => { afterEach(async () => {
const needs = await determineAgentMetadataNeeds({ process.env.CODEX_SESSION_DIR = previousCodexSessionDir;
cwd: "/tmp", await shutdownProviders(logger);
initialPrompt: "Do the thing", rmSync(repoDir, { recursive: true, force: true });
explicitTitle: null, rmSync(paseoHome, { recursive: true, force: true });
deps: { rmSync(codexSessionDir, { recursive: true, force: true });
getCheckoutStatus: async () => ({ }, 60000);
isGit: false,
}),
},
});
expect(needs.needsTitle).toBe(true); test(
expect(needs.needsBranch).toBe(false); "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");
it("schedules async title generation", async () => { await generateAndApplyAgentMetadata({
const agentManager = createAgentManagerStub(); agentManager: manager,
const generator = vi.fn(async () => ({ title: "Generated Title" })); agentId: agent.id,
cwd: repoDir,
initialPrompt: "Use the exact title 'Metadata Title E2E'.",
explicitTitle: null,
paseoHome,
logger,
});
scheduleAgentMetadataGeneration({ await storage.flush();
agentManager, const record = await storage.get(agent.id);
agentId: "agent-2", expect(record?.title).toBe("Metadata Title E2E");
cwd: "/tmp",
initialPrompt: "Create a report",
explicitTitle: null,
logger,
deps: {
generateStructuredAgentResponse: generator,
getCheckoutStatus: async () => ({
isGit: false,
}),
},
});
expect(generator).not.toHaveBeenCalled(); await manager.closeAgent(agent.id);
},
180000
);
await delayImmediate(); 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,
});
expect(generator).toHaveBeenCalledTimes(1); const agent = await manager.createAgent({
expect(agentManager.setTitle).toHaveBeenCalledWith( provider: "codex",
"agent-2", model: CODEX_TEST_MODEL,
"Generated Title" reasoningEffort: CODEX_TEST_REASONING_EFFORT,
); modeId: "auto",
}); cwd: worktree.worktreePath,
title: "Worktree Agent",
}, "metadata-branch-agent");
it("selects title-only schema when branch is not eligible", async () => { await generateAndApplyAgentMetadata({
const agentManager = createAgentManagerStub(); agentManager: manager,
let capturedSchema: unknown; agentId: agent.id,
const generator = vi.fn(async (options: any) => { cwd: worktree.worktreePath,
capturedSchema = options.schema; initialPrompt: "Use the exact branch 'feat/metadata-worktree'.",
return { title: "Only Title" }; explicitTitle: "Explicit Title",
}); paseoHome,
logger,
});
await generateAndApplyAgentMetadata({ const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
agentManager, cwd: worktree.worktreePath,
agentId: "agent-title-only", stdio: "pipe",
cwd: "/tmp", }).toString().trim();
initialPrompt: "Do the thing",
explicitTitle: null,
logger,
deps: {
generateStructuredAgentResponse: generator,
getCheckoutStatus: async () => ({
isGit: false,
}),
},
});
expect(getSchemaKeys(capturedSchema)).toEqual(["title"]); const validation = validateBranchSlug(currentBranch);
}); expect(validation.valid).toBe(true);
expect(currentBranch).toBe("feat/metadata-worktree");
it("selects branch-only schema when explicit title exists and branch is eligible", async () => { await manager.closeAgent(agent.id);
const agentManager = createAgentManagerStub(); },
let capturedSchema: unknown; 180000
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",
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"]);
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,37 +1334,15 @@ 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 {