mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-21 00:32:45 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
060d521340 | ||
|
|
da5e8fbce3 |
@@ -5487,6 +5487,7 @@ describe("Codex app-server provider", () => {
|
||||
method: "thread/goal/set",
|
||||
params: {
|
||||
threadId: "test-thread",
|
||||
origin: "user",
|
||||
objective: "ship feature",
|
||||
status: "active",
|
||||
},
|
||||
|
||||
@@ -5182,6 +5182,7 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
case "set": {
|
||||
await this.client.request("thread/goal/set", {
|
||||
threadId: this.currentThreadId,
|
||||
origin: "user",
|
||||
objective: subcommand.objective,
|
||||
status: "active",
|
||||
});
|
||||
@@ -5190,6 +5191,7 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
case "pause": {
|
||||
await this.client.request("thread/goal/set", {
|
||||
threadId: this.currentThreadId,
|
||||
origin: "user",
|
||||
status: "paused",
|
||||
});
|
||||
return "Goal paused.";
|
||||
@@ -5197,6 +5199,7 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
case "resume": {
|
||||
await this.client.request("thread/goal/set", {
|
||||
threadId: this.currentThreadId,
|
||||
origin: "user",
|
||||
status: "active",
|
||||
});
|
||||
return "Goal resumed.";
|
||||
@@ -5204,6 +5207,7 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
case "clear": {
|
||||
await this.client.request("thread/goal/clear", {
|
||||
threadId: this.currentThreadId,
|
||||
origin: "user",
|
||||
});
|
||||
return "Goal cleared.";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { once } from "node:events";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import path from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||
import { CodexAppServerAgentClient } from "./codex-app-server-agent.js";
|
||||
import type { AgentSession, AgentStreamEvent } from "../agent-sdk-types.js";
|
||||
|
||||
// Native Codex owns goals; the fixture supplies inference responses only.
|
||||
test("records all four /goal commands as user edits in the next model request", async () => {
|
||||
const root = mkdtempSync(path.resolve(".dev-goal-provenance-"));
|
||||
const requests: string[] = [];
|
||||
function handleRequest(req: IncomingMessage, res: ServerResponse): void {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
req.on("end", () => {
|
||||
if (req.method !== "POST" || req.url !== "/v1/responses") {
|
||||
res.writeHead(404).end();
|
||||
return;
|
||||
}
|
||||
requests.push(Buffer.concat(chunks).toString("utf8"));
|
||||
res.setHeader("content-type", "text/event-stream");
|
||||
const id = `resp-${requests.length}`;
|
||||
const events = [
|
||||
{ type: "response.created", response: { id } },
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
id: `msg-${requests.length}`,
|
||||
content: [{ type: "output_text", text: "LOCAL_FIXTURE_RESPONSE" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "response.completed",
|
||||
response: {
|
||||
id,
|
||||
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
|
||||
},
|
||||
},
|
||||
];
|
||||
res.end(events.map((e) => `event: ${e.type}\ndata: ${JSON.stringify(e)}\n\n`).join(""));
|
||||
});
|
||||
}
|
||||
const server = createServer(handleRequest);
|
||||
let session: AgentSession | null = null;
|
||||
try {
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("missing fixture address");
|
||||
writeFileSync(
|
||||
path.join(root, "config.toml"),
|
||||
`
|
||||
model = "gpt-6.1-sol"
|
||||
model_reasoning_effort = "low"
|
||||
model_provider = "fixture"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
[features]
|
||||
enable_request_compression = false
|
||||
[model_providers.fixture]
|
||||
name = "Local response fixture"
|
||||
base_url = "http://127.0.0.1:${address.port}/v1"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = false
|
||||
supports_websockets = false
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
`,
|
||||
);
|
||||
const client = new CodexAppServerAgentClient(createTestLogger(), { env: { CODEX_HOME: root } });
|
||||
session = await client.createSession({
|
||||
provider: "codex",
|
||||
cwd: root,
|
||||
model: "gpt-6.1-sol",
|
||||
thinkingOptionId: "low",
|
||||
});
|
||||
let active = false;
|
||||
const unsubscribe = session.subscribe((event) => {
|
||||
if (event.type === "turn_started") active = true;
|
||||
if (["turn_completed", "turn_canceled", "turn_failed"].includes(event.type)) active = false;
|
||||
});
|
||||
try {
|
||||
expect((await session.listCommands?.())?.some((command) => command.name === "goal")).toBe(
|
||||
true,
|
||||
);
|
||||
for (const command of [
|
||||
"/goal AUDIT_GOAL: summarize local evidence",
|
||||
"/goal pause",
|
||||
"/goal resume",
|
||||
"/goal clear",
|
||||
]) {
|
||||
const handler = session.tryHandleOutOfBand?.(command);
|
||||
expect(handler).toBeTruthy();
|
||||
const events: AgentStreamEvent[] = [];
|
||||
await handler!.run({ emit: (event) => events.push(event) });
|
||||
expect(events).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
item: expect.objectContaining({
|
||||
text: expect.stringContaining("Failed to update goal"),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
}
|
||||
await expect.poll(() => active, { timeout: 30_000 }).toBe(false);
|
||||
expect((await session.run("PROBE_AFTER_GOAL_CLEAR")).finalText).toBe(
|
||||
"LOCAL_FIXTURE_RESPONSE",
|
||||
);
|
||||
const body = JSON.parse(requests.at(-1)!);
|
||||
const goalContext = body.input
|
||||
.filter((item: { role: string }) => item.role === "user")
|
||||
.flatMap((item: { content: Array<{ text?: string }> }) =>
|
||||
item.content.map((part) => part.text ?? ""),
|
||||
)
|
||||
.filter((text: string) => text.includes('<codex_internal_context source="user_goal">'))
|
||||
.join("\n");
|
||||
expect(goalContext).toContain('User set the goal: "AUDIT_GOAL: summarize local evidence"');
|
||||
expect(goalContext).toContain('User set goal status: "paused".');
|
||||
expect(goalContext).toContain('User set goal status: "active".');
|
||||
expect(goalContext).toContain("User cleared the goal.");
|
||||
} finally {
|
||||
unsubscribe();
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await session?.close();
|
||||
} finally {
|
||||
try {
|
||||
server.closeAllConnections();
|
||||
if (server.listening) {
|
||||
server.close();
|
||||
await once(server, "close");
|
||||
}
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 60_000);
|
||||
@@ -0,0 +1,84 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||
import { CodexAppServerAgentClient } from "./codex-app-server-agent.js";
|
||||
import type { AgentSession } from "../agent-sdk-types.js";
|
||||
|
||||
// A protocol peer exercises the public session interface without a Codex install.
|
||||
test("marks set, pause, resume and clear as user goal mutations", async () => {
|
||||
const root = mkdtempSync(path.resolve(".dev-goal-protocol-"));
|
||||
const script = path.join(root, "codex.cjs");
|
||||
const capture = path.join(root, "requests.jsonl");
|
||||
writeFileSync(
|
||||
script,
|
||||
`
|
||||
const fs = require("node:fs");
|
||||
const readline = require("node:readline");
|
||||
if (process.argv.includes("--version")) {
|
||||
console.log("codex-cli 0.162.1");
|
||||
process.exit(0);
|
||||
}
|
||||
readline.createInterface({ input: process.stdin }).on("line", (line) => {
|
||||
const request = JSON.parse(line);
|
||||
if (request.id === undefined) return;
|
||||
fs.appendFileSync(process.env.CAPTURE, JSON.stringify(request) + "\\n");
|
||||
let result = {};
|
||||
if (request.method === "initialize") result = { userAgent: "codex-cli/0.162.1" };
|
||||
if (request.method === "thread/start") result = { thread: { id: "goal-thread" } };
|
||||
if (request.method === "thread/loaded/list") result = { data: ["goal-thread"] };
|
||||
process.stdout.write(JSON.stringify({ id: request.id, result }) + "\\n");
|
||||
});
|
||||
`,
|
||||
);
|
||||
const client = new CodexAppServerAgentClient(createTestLogger(), {
|
||||
command: { mode: "replace", argv: [process.execPath, script] },
|
||||
env: { CODEX_HOME: root, CAPTURE: capture },
|
||||
});
|
||||
let session: AgentSession | null = null;
|
||||
try {
|
||||
session = await client.createSession({
|
||||
provider: "codex",
|
||||
cwd: root,
|
||||
model: "test-model",
|
||||
thinkingOptionId: "low",
|
||||
});
|
||||
for (const command of ["/goal ship feature", "/goal pause", "/goal resume", "/goal clear"]) {
|
||||
const handler = session.tryHandleOutOfBand?.(command);
|
||||
if (!handler) throw new Error(`Missing handler for ${command}`);
|
||||
await handler.run({ emit: () => {} });
|
||||
}
|
||||
const requests = readFileSync(capture, "utf8")
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
const mutations = requests.filter((request) => request.method.startsWith("thread/goal/"));
|
||||
expect(mutations.map(({ method, params }) => ({ method, params }))).toEqual([
|
||||
{
|
||||
method: "thread/goal/set",
|
||||
params: {
|
||||
threadId: "goal-thread",
|
||||
origin: "user",
|
||||
objective: "ship feature",
|
||||
status: "active",
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "thread/goal/set",
|
||||
params: { threadId: "goal-thread", origin: "user", status: "paused" },
|
||||
},
|
||||
{
|
||||
method: "thread/goal/set",
|
||||
params: { threadId: "goal-thread", origin: "user", status: "active" },
|
||||
},
|
||||
{ method: "thread/goal/clear", params: { threadId: "goal-thread", origin: "user" } },
|
||||
]);
|
||||
} finally {
|
||||
try {
|
||||
await session?.close();
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user