修复策划 Agent CI 回归

按 rustfmt 整理策划 Runtime 与相关测试字面量
首页做方案用例改为断言设计 Agent 命令
This commit is contained in:
2026-09-10 11:19:24 +00:00
parent a38eec743a
commit 54ae60576d
9 changed files with 649 additions and 244 deletions
@@ -12,6 +12,8 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
mod codex_app_server;
mod codex_cli;
mod codex_provider_proxy;
mod design_runtime;
mod design_tools;
mod direct_codex_attachments;
mod direct_codex_audit;
mod direct_project_history;
@@ -19,8 +21,6 @@ mod direct_project_turn_history;
mod direct_runtime;
mod direct_tool_bridge;
mod direct_tools_mcp;
mod design_runtime;
mod design_tools;
mod generation;
mod interaction;
mod prompt;
@@ -40,6 +40,7 @@ pub(crate) use codex_cli::{
game_creator_codex_cli_executable_path, game_creator_codex_cli_version_identity,
};
pub(crate) use codex_provider_proxy::*;
pub(crate) use design_runtime::*;
pub(crate) use direct_codex_attachments::*;
pub(crate) use direct_codex_audit::*;
pub(crate) use direct_project_history::*;
@@ -47,7 +48,6 @@ pub(crate) use direct_project_turn_history::*;
pub(crate) use direct_runtime::*;
pub(crate) use direct_tool_bridge::*;
pub(crate) use direct_tools_mcp::*;
pub(crate) use design_runtime::*;
pub(crate) use generation::*;
pub(crate) use interaction::*;
pub(crate) use prompt::*;
@@ -3078,8 +3078,8 @@ fn parse_game_creator_codex_app_server_text(
response_id: Some(thread_id.to_string()),
usage: None,
tool_calls,
responses_output: Vec::new(),
})
responses_output: Vec::new(),
})
}
async fn read_game_creator_codex_app_server_stdout(
@@ -593,8 +593,8 @@ fn parse_game_creator_codex_cli_response(
response_id,
usage,
tool_calls,
responses_output: Vec::new(),
})
responses_output: Vec::new(),
})
}
async fn request_game_creator_agent_codex_cli_with_executable(
File diff suppressed because it is too large Load Diff
@@ -92,10 +92,7 @@ impl DesignResources {
items.sort_by(|left, right| left.id.cmp(&right.id));
lines.push(format!("\n{category}"));
for item in items {
lines.push(format!(
"- {}{}{}",
item.id, item.title, item.summary
));
lines.push(format!("- {}{}{}", item.id, item.title, item.summary));
}
}
Ok(lines.join("\n"))
@@ -228,7 +225,11 @@ pub(crate) fn execute_design_file_tool(
let name = workspace_display_path(&relative, &entry.file_name().to_string_lossy());
rows.push(format!(
"{} {name}",
if child.is_dir() { "[目录]" } else { "[文件]" }
if child.is_dir() {
"[目录]"
} else {
"[文件]"
}
));
}
Ok(Value::String(if rows.is_empty() {
@@ -276,10 +277,17 @@ pub(crate) fn execute_design_file_tool(
}
let (display, path) = resolve_design_workspace_path(root, &relative)?;
if !path.is_file() {
return Ok(Value::String(format!("局部修改失败:文件不存在:{display}")));
return Ok(Value::String(format!(
"局部修改失败:文件不存在:{display}"
)));
}
let content = fs::read_to_string(&path).map_err(|error| format!("读取失败:{error}"))?;
let newline = if content.contains("\r\n") { "\r\n" } else { "\n" };
let content =
fs::read_to_string(&path).map_err(|error| format!("读取失败:{error}"))?;
let newline = if content.contains("\r\n") {
"\r\n"
} else {
"\n"
};
let old = old.replace("\r\n", "\n").replace('\n', newline);
let new = new.replace("\r\n", "\n").replace('\n', newline);
let count = content.matches(&old).count();
@@ -453,7 +461,8 @@ fn load_design_tools(root: &Path) -> Result<Vec<platform_llm::LlmFunctionTool>,
}
fn read_pack_text(root: &Path, relative: &str) -> Result<String, String> {
fs::read_to_string(root.join(relative)).map_err(|error| format!("读取 {relative} 失败:{error}"))
fs::read_to_string(root.join(relative))
.map_err(|error| format!("读取 {relative} 失败:{error}"))
}
fn optional_tool_path(args: &Value) -> Result<String, String> {
@@ -119,8 +119,8 @@ fn persist_tool_plan_handoff_repair_chain(
response_id: None,
usage: None,
tool_calls: Vec::new(),
responses_output: Vec::new(),
};
responses_output: Vec::new(),
};
tool_plan_handoff::write_at(
root,
&base_identity,
@@ -1120,8 +1120,8 @@ async fn tool_plan_handoff_identity_drift_closes_entire_repair_chain_before_remo
response_id: None,
usage: None,
tool_calls: Vec::new(),
responses_output: Vec::new(),
};
responses_output: Vec::new(),
};
tool_plan_handoff::write_at(
root,
&base_identity,
@@ -1440,8 +1440,8 @@ async fn tool_plan_handoff_durable_control_closes_entire_repair_chain_before_rem
response_id: None,
usage: None,
tool_calls: Vec::new(),
responses_output: Vec::new(),
};
responses_output: Vec::new(),
};
tool_plan_handoff::write_at(
root,
&base_identity,
@@ -1,10 +1,10 @@
use super::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use uuid::Uuid;
use serde_json::Value;
pub(crate) const DESIGN_SESSION_SCHEMA_VERSION: &str = "design-agent-session.v1";
pub(crate) const DESIGN_SESSION_ENGINE: &str = "design-agent";
@@ -181,8 +181,14 @@ pub(crate) fn validate_design_session(session: &DesignSession) -> Result<(), Str
pub(crate) fn read_design_session(root: &Path) -> Result<Option<DesignSession>, String> {
let Some(session) = read_agent_runtime_json_sidecar_with_max_bytes::<DesignSession>(
root, DESIGN_SESSION_PATH, "策划 Agent 会话", DESIGN_SESSION_MAX_BYTES,
)? else { return Ok(None); };
root,
DESIGN_SESSION_PATH,
"策划 Agent 会话",
DESIGN_SESSION_MAX_BYTES,
)?
else {
return Ok(None);
};
validate_design_session(&session)?;
Ok(Some(session))
}
@@ -190,7 +196,11 @@ pub(crate) fn read_design_session(root: &Path) -> Result<Option<DesignSession>,
pub(crate) fn write_design_session(root: &Path, session: &DesignSession) -> Result<(), String> {
validate_design_session(session)?;
write_agent_runtime_json_sidecar_with_max_bytes(
root, DESIGN_SESSION_PATH, "策划 Agent 会话", session, DESIGN_SESSION_MAX_BYTES,
root,
DESIGN_SESSION_PATH,
"策划 Agent 会话",
session,
DESIGN_SESSION_MAX_BYTES,
)
}
@@ -199,7 +209,9 @@ pub(crate) fn ensure_design_session(
project_id: &str,
) -> Result<DesignSession, String> {
if let Some(session) = read_design_session(root)? {
if session.project_id != project_id { return Err("策划会话与当前项目不匹配".to_string()); }
if session.project_id != project_id {
return Err("策划会话与当前项目不匹配".to_string());
}
return Ok(session);
}
let session = new_design_session(project_id.to_string());
@@ -244,7 +256,9 @@ pub(crate) fn submit_design_phase_for_approval(
if session.current_phase == "consultant" {
return Err("顾问态不提交阶段审批".to_string());
}
if let Some(request) = &session.pending_approval { return Ok(request.clone()); }
if let Some(request) = &session.pending_approval {
return Ok(request.clone());
}
let missing = check_design_phase_artifacts(root, session, &session.current_phase)?;
if !missing.is_empty() {
return Err(format!("缺少必需产物:{}", missing.join("")));
@@ -274,8 +288,10 @@ pub(crate) fn approve_design_phase(
}
session.approved_phases.push(session.current_phase.clone());
let next = design_phase_index(&session.current_phase)? + 1;
session.current_phase = DESIGN_PHASES.get(next)
.ok_or_else(|| "顾问态没有下一阶段".to_string())?.to_string();
session.current_phase = DESIGN_PHASES
.get(next)
.ok_or_else(|| "顾问态没有下一阶段".to_string())?
.to_string();
session.pending_clarification = None;
session.updated_at = unix_timestamp();
Ok(session.current_phase.clone())
@@ -2779,8 +2779,8 @@ fn durable_provider_handoff_prevents_shutdown_even_when_corrupt() {
response_id: Some("provider-handoff-response".to_string()),
usage: None,
tool_calls: Vec::new(),
responses_output: Vec::new(),
};
responses_output: Vec::new(),
};
let provider_request_id = format!("provider-request-{}", "f".repeat(64));
crate::provider_handoff::write_at(
&root,
@@ -27,30 +27,35 @@ import {
within,
} from './harness';
function emptyPlanningV2StartResult(projectId = 'local-project-draft') {
function homeDesignContinueView({
prompt,
pendingClarification = null,
}: {
prompt: string;
pendingClarification?: {
requestId: string;
question: string;
options: string[];
createdAt: number;
} | null;
}) {
return {
session: {
schemaVersion: 'planning-session.v2',
engine: 'planning-session-v2',
sessionId: 'home-planning-v2-session',
projectId,
mode: 'gdd',
status: 'planning',
sessionId: 'design-session-home',
projectId: 'local-project-draft',
currentPhase: 'concept',
approvedPhases: [],
pendingApproval: null,
pendingClarification,
turnIndex: 1,
questionCount: 0,
questionLimit: 8,
revisionCount: 0,
currentArtifactVersion: null,
currentQuestion: null,
capabilities: { tools: [], skills: [] },
processingSeconds: 0.5,
createdAtUtc: '2026-09-03T00:00:00Z',
updatedAtUtc: '2026-09-03T00:00:01Z',
lastError: null,
},
result: null,
currentArtifact: null,
replayed: false,
messages: [
{ id: 'u1', role: 'user', text: prompt },
{ id: 'a1', role: 'assistant', text: '先确认核心循环。' },
],
running: false,
canRetry: false,
};
}
@@ -1784,7 +1789,7 @@ export function registerHomeProjectCreationTests() {
['做方案', false],
['做方案', true],
] as const)(
'routes %s %s creation to Planning Session V2',
'routes %s %s creation to the design agent',
async (modeLabel, automatic) => {
const projectPath = `/tmp/home-${modeLabel}-${automatic ? 'enter' : 'submit'}`;
const manifest = createGameCreationAppManifest(
@@ -1793,7 +1798,9 @@ export function registerHomeProjectCreationTests() {
);
const supervisorHarness = createProjectSupervisorRuntimeHarness({
projectPath,
planningV2StartResult: emptyPlanningV2StartResult(),
designAgentContinueView: homeDesignContinueView({
prompt: '整理一个可玩原型',
}),
});
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
@@ -1840,20 +1847,28 @@ export function registerHomeProjectCreationTests() {
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'start_planning_session_v2',
'continue_design_agent_session',
expect.objectContaining({
projectPath,
mode: 'gdd',
input: expect.objectContaining({
type: 'message',
text: '整理一个可玩原型',
}),
}),
);
});
const startCall = invoke.mock.calls.find(
([command]) => command === 'start_planning_session_v2',
([command]) => command === 'continue_design_agent_session',
);
expect(startCall?.[1]).not.toHaveProperty('attachments');
expect(JSON.stringify(startCall?.[1] ?? {})).not.toContain(
'本轮用户附件',
);
expect(
invoke.mock.calls.some(
([command]) => command === 'start_planning_session_v2',
),
).toBe(false);
expect(invoke).not.toHaveBeenCalledWith(
'chat_with_game_creator_direct_codex',
expect.anything(),
@@ -1882,9 +1897,9 @@ export function registerHomeProjectCreationTests() {
);
const supervisorHarness = createProjectSupervisorRuntimeHarness({
projectPath,
planningV2StartResult: emptyPlanningV2StartResult(
'home-planning-attachment',
),
designAgentContinueView: homeDesignContinueView({
prompt: '整理一份可玩原型',
}),
});
const fileBytes = Array.from(new TextEncoder().encode('png'));
const invoke = vi.fn(
@@ -1937,10 +1952,10 @@ export function registerHomeProjectCreationTests() {
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'start_planning_session_v2',
'continue_design_agent_session',
expect.objectContaining({
projectPath,
mode: 'gdd',
input: expect.objectContaining({ type: 'message' }),
}),
);
});
@@ -1951,13 +1966,18 @@ export function registerHomeProjectCreationTests() {
bytes: fileBytes,
});
const startCall = invoke.mock.calls.find(
([command]) => command === 'start_planning_session_v2',
([command]) => command === 'continue_design_agent_session',
);
expect(startCall?.[1]).not.toHaveProperty('attachments');
expect(JSON.stringify(startCall?.[1] ?? {})).not.toContain('本轮用户附件');
expect(JSON.stringify(startCall?.[1] ?? {})).not.toContain(
'assets/uploads/reference.png',
);
expect(
invoke.mock.calls.some(
([command]) => command === 'start_planning_session_v2',
),
).toBe(false);
expect(invoke).not.toHaveBeenCalledWith(
'chat_with_game_creator_direct_codex',
expect.anything(),
@@ -1980,53 +2000,15 @@ export function registerHomeProjectCreationTests() {
);
const supervisorHarness = createProjectSupervisorRuntimeHarness({
projectPath,
expectedRunProfile: 'standard',
});
supervisorHarness.setPlanningV2StartResult({
session: {
schemaVersion: 'planning-session.v2',
engine: 'planning-session-v2',
sessionId: 'home-planning-v2-session',
projectId: 'local-project-draft',
mode: 'gdd',
status: 'awaiting_user',
turnIndex: 1,
questionCount: 1,
questionLimit: 8,
revisionCount: 0,
currentArtifactVersion: null,
currentQuestion: {
id: 'visual_direction',
header: '当前要决定:首版美术方向',
designAgentContinueView: homeDesignContinueView({
prompt: '2D射击游戏',
pendingClarification: {
requestId: 'visual_direction',
question: '首版角色规范图采用哪种美术方向?',
options: [
{ label: '像素', description: '低成本像素风。' },
{ label: '扁平', description: '清晰的扁平插画风。' },
],
options: ['像素', '扁平'],
createdAt: 1,
},
capabilities: { tools: [], skills: [] },
processingSeconds: 1,
createdAtUtc: '2026-09-03T00:00:00Z',
updatedAtUtc: '2026-09-03T00:00:01Z',
lastError: null,
},
result: {
schemaVersion: 'planning-turn-result.v2',
kind: 'question',
payload: {
question: {
id: 'visual_direction',
header: '当前要决定:首版美术方向',
question: '首版角色规范图采用哪种美术方向?',
options: [
{ label: '像素', description: '低成本像素风。' },
{ label: '扁平', description: '清晰的扁平插画风。' },
],
},
},
},
currentArtifact: null,
replayed: false,
}),
});
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
@@ -2063,15 +2045,24 @@ export function registerHomeProjectCreationTests() {
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'start_planning_session_v2',
expect.objectContaining({ prompt: '2D射击游戏', mode: 'gdd' }),
'continue_design_agent_session',
expect.objectContaining({
projectPath,
input: expect.objectContaining({
type: 'message',
text: '2D射击游戏',
}),
}),
);
});
const strip = await screen.findByLabelText('立项策划运行状态');
expect(
within(strip).getByText('首版角色规范图采用哪种美术方向?'),
).not.toBeNull();
invoke.mock.calls.some(
([command]) => command === 'start_planning_session_v2',
),
).toBe(false);
await screen.findByText('首版角色规范图采用哪种美术方向?');
expect(screen.getByRole('button', { name: '像素' })).not.toBeNull();
});
it('refreshes Direct Codex art commits while the turn is still running and after a later failure', async () => {
const projectPath =