97d8a6c39a
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 7m34s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 7m42s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 7m43s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 8m1s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m58s
Project CI / AI game creator shell Rust crates (push) Successful in 3m9s
Project CI / Backend tests (push) Successful in 12m4s
Project CI / Frontend tests (push) Successful in 11m37s
Project CI / Native shell tests (push) Successful in 15m37s
Project CI / Repository checks (push) Successful in 13m51s
Project CI / AI game creator shell web tests (push) Successful in 6m52s
将智能体系统指令、工具说明和恢复模板集中到外置提示词目录 精简重复否定、历史对照与无关实现说明,保留必要执行约束 扩展提示词构建校验和源码边界检查并接入现有本地与持续集成门禁 同步内置技能、协作规范、技术文档与对应验证断言
524 lines
18 KiB
Rust
524 lines
18 KiB
Rust
use std::ops::Range;
|
|
use std::path::Path;
|
|
|
|
fn rust_sources(directory: &Path, files: &mut Vec<std::path::PathBuf>) {
|
|
for entry in std::fs::read_dir(directory).expect("source directory") {
|
|
let path = entry.expect("source entry").path();
|
|
if path.is_dir() {
|
|
rust_sources(&path, files);
|
|
} else if path.extension().is_some_and(|extension| extension == "rs") {
|
|
files.push(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn prompt_references_in_all_feature_branches_resolve_to_registered_texts() {
|
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
let bundle = root.join("prompts/runtime");
|
|
let manifest: serde_json::Value =
|
|
serde_json::from_str(&read(&bundle.join("manifest.json"))).expect("manifest");
|
|
let mut keys = std::collections::BTreeSet::new();
|
|
for (catalog, path) in manifest["textCatalogs"].as_object().expect("text catalogs") {
|
|
let texts: serde_json::Value =
|
|
serde_json::from_str(&read(&bundle.join(path.as_str().expect("catalog path"))))
|
|
.expect("text catalog");
|
|
keys.extend(
|
|
texts
|
|
.as_object()
|
|
.expect("text entries")
|
|
.keys()
|
|
.map(|key| format!("{catalog}.{key}")),
|
|
);
|
|
}
|
|
let mut sources = Vec::new();
|
|
rust_sources(&root.join("src"), &mut sources);
|
|
for path in sources {
|
|
let source = read(&path);
|
|
let (masked, literals) = mask_literals_and_comments(&source);
|
|
for literal in literals {
|
|
let prefix = masked[..literal.start].trim_end();
|
|
if !prefix.ends_with("prompt_text!(") {
|
|
continue;
|
|
}
|
|
let key: String =
|
|
serde_json::from_str(&source[literal.clone()]).expect("literal prompt key");
|
|
assert!(
|
|
keys.contains(&key),
|
|
"{} references unregistered prompt key {key}",
|
|
path.display()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn read(path: &Path) -> String {
|
|
std::fs::read_to_string(path)
|
|
.expect("read prompt source")
|
|
.replace("\r\n", "\n")
|
|
}
|
|
|
|
fn mask_literals_and_comments(source: &str) -> (String, Vec<Range<usize>>) {
|
|
let bytes = source.as_bytes();
|
|
let mut masked = bytes.to_vec();
|
|
let mut literals = Vec::new();
|
|
let mut index = 0;
|
|
while index < bytes.len() {
|
|
let start = index;
|
|
let mut literal = false;
|
|
if bytes[index..].starts_with(b"//") {
|
|
index = source[index..]
|
|
.find('\n')
|
|
.map_or(bytes.len(), |end| index + end);
|
|
} else if bytes[index..].starts_with(b"/*") {
|
|
index += 2;
|
|
let mut depth = 1;
|
|
while index < bytes.len() && depth > 0 {
|
|
if bytes[index..].starts_with(b"/*") {
|
|
depth += 1;
|
|
index += 2;
|
|
} else if bytes[index..].starts_with(b"*/") {
|
|
depth -= 1;
|
|
index += 2;
|
|
} else {
|
|
index += 1;
|
|
}
|
|
}
|
|
} else if bytes[index] == b'r' && matches!(bytes.get(index + 1), Some(b'#' | b'"')) {
|
|
let hashes = bytes[index + 1..]
|
|
.iter()
|
|
.take_while(|byte| **byte == b'#')
|
|
.count();
|
|
let quote = index + 1 + hashes;
|
|
if bytes.get(quote) != Some(&b'"') {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
let closing = format!("\"{}", "#".repeat(hashes));
|
|
index = quote
|
|
+ 1
|
|
+ source[quote + 1..]
|
|
.find(&closing)
|
|
.expect("closed raw literal")
|
|
+ closing.len();
|
|
literal = true;
|
|
} else if bytes[index] == b'"' {
|
|
index += 1;
|
|
while index < bytes.len() {
|
|
if bytes[index] == b'\\' {
|
|
index += 2;
|
|
} else if bytes[index] == b'"' {
|
|
index += 1;
|
|
break;
|
|
} else {
|
|
index += 1;
|
|
}
|
|
}
|
|
literal = true;
|
|
} else if bytes[index] == b'\'' {
|
|
let length = if bytes.get(index + 1) == Some(&b'\\') {
|
|
2
|
|
} else {
|
|
source[index + 1..].chars().next().map_or(0, char::len_utf8)
|
|
};
|
|
if bytes.get(index + 1 + length) == Some(&b'\'') {
|
|
index += length + 2;
|
|
} else {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
} else {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if literal {
|
|
literals.push(start..index);
|
|
}
|
|
masked[start..index].fill(b' ');
|
|
}
|
|
(String::from_utf8(masked).expect("masked UTF-8"), literals)
|
|
}
|
|
|
|
fn function_range(masked: &str, name: &str) -> Range<usize> {
|
|
let signature = masked.find(&format!("fn {name}(")).expect(name);
|
|
let start = signature + masked[signature..].find('{').expect("function body");
|
|
let mut depth = 0;
|
|
for (offset, byte) in masked.as_bytes()[start..].iter().enumerate() {
|
|
match byte {
|
|
b'{' => depth += 1,
|
|
b'}' => depth -= 1,
|
|
_ => {}
|
|
}
|
|
if depth == 0 {
|
|
return start..start + offset + 1;
|
|
}
|
|
}
|
|
panic!("unclosed function {name}");
|
|
}
|
|
|
|
fn is_prose(literal: &str) -> bool {
|
|
if literal
|
|
.chars()
|
|
.any(|ch| ('\u{3400}'..='\u{9fff}').contains(&ch))
|
|
{
|
|
return true;
|
|
}
|
|
let mut words = 0;
|
|
for word in literal.split_whitespace() {
|
|
let word = word.trim_matches(|ch: char| ch.is_ascii_punctuation());
|
|
if !word.is_empty() && word.chars().all(|ch| ch.is_ascii_alphabetic()) {
|
|
words += 1;
|
|
if words >= 6 {
|
|
return true;
|
|
}
|
|
} else {
|
|
words = 0;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
#[test]
|
|
fn prompt_entry_points_load_their_prose_from_external_files() {
|
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
let cases: &[(&str, &[&str])] = &[
|
|
(
|
|
"src/agent/direct_runtime/mod.rs",
|
|
&[
|
|
"direct_engine_three_dimensional_contract",
|
|
"direct_engine_three_dimensional_home_note",
|
|
"direct_codex_error_feedback_prompt",
|
|
"direct_browser_evidence_prompt",
|
|
"build_direct_codex_system_prompt_with_search",
|
|
"build_direct_codex_home_system_prompt",
|
|
],
|
|
),
|
|
(
|
|
"src/agent/prompt.rs",
|
|
&[
|
|
"render_agent_runtime_prompt_context_for_session",
|
|
"game_creator_chat_agent_system_prompt",
|
|
"game_creator_role_agent_chat_system_prompt",
|
|
"game_creator_design_foundation_tool_plan_prompt",
|
|
"game_creator_art_director_tool_plan_prompt",
|
|
"game_creator_art_asset_plan_tool_plan_prompt",
|
|
"game_creator_agent_runtime_tool_plan_system_prompt_for_platform",
|
|
],
|
|
),
|
|
(
|
|
"src/agent/generation/prompt_context.rs",
|
|
&[
|
|
"game_creator_system_prompt",
|
|
"game_creator_planner_system_prompt",
|
|
"game_creator_planner_user_prompt",
|
|
"game_creator_generator_user_prompt",
|
|
"render_local_asset_prompt_context",
|
|
],
|
|
),
|
|
(
|
|
"src/agent/runtime_actions/provider_request_builders.rs",
|
|
&[
|
|
"provider_command_exec_contract",
|
|
"provider_command_start_contract",
|
|
"game_creator_agent_context_preload_notice",
|
|
],
|
|
),
|
|
("src/agent/design_tools.rs", &["phase_context"]),
|
|
(
|
|
"src/agent_native_tools.rs",
|
|
&[
|
|
"runtime_tool_description",
|
|
"runtime_tool_input_schema",
|
|
"plan_update_function_tool",
|
|
"response_function_tool",
|
|
],
|
|
),
|
|
(
|
|
"src/agent/interaction.rs",
|
|
&[
|
|
"agent_runtime_steer_decision_function_tool",
|
|
"agent_interaction_system_prompt",
|
|
"build_agent_interaction_llm_request",
|
|
],
|
|
),
|
|
(
|
|
"src/context_compaction.rs",
|
|
&[
|
|
"build_game_creator_agent_runtime_context_compaction_request",
|
|
"merge_context_compaction_summary_with_pinned_constraints",
|
|
],
|
|
),
|
|
(
|
|
"src/agent/runtime_actions/provider_tool_plan.rs",
|
|
&[
|
|
"provider_collaboration_repair_instruction",
|
|
"root_goal_contract_repair_instruction",
|
|
],
|
|
),
|
|
(
|
|
"src/repository_context.rs",
|
|
&["render_repository_startup_context_for_prompt"],
|
|
),
|
|
(
|
|
"src/goal.rs",
|
|
&[
|
|
"render_agent_goal_prompt_context_at",
|
|
"render_agent_goal_edit_instruction",
|
|
],
|
|
),
|
|
(
|
|
"src/agent/direct_codex_user_item/wire.rs",
|
|
&["render_ui_design_code_context"],
|
|
),
|
|
(
|
|
"src/commands.rs",
|
|
&["build_local_project_prompt_polish_prompt"],
|
|
),
|
|
(
|
|
"src/agent/generation/run_lifecycle.rs",
|
|
&["resumed_agent_run_prompt"],
|
|
),
|
|
(
|
|
"src/agent/codex_cli.rs",
|
|
&[
|
|
"game_creator_codex_cli_tool_output_schema",
|
|
"render_game_creator_codex_cli_prompt",
|
|
],
|
|
),
|
|
(
|
|
"src/agent/runtime_protocol/autonomous_completion.rs",
|
|
&["autonomous_playtest_contract_prompt"],
|
|
),
|
|
(
|
|
"src/agent/runtime_driver/task_start.rs",
|
|
&[
|
|
"render_autonomous_manifest_ready_task_owner_prompt",
|
|
"render_autonomous_manifest_ready_task_background_prompt",
|
|
"render_relaxed_autonomous_manifest_ready_task_background_prompt",
|
|
"render_manifest_ready_task_background_prompt",
|
|
],
|
|
),
|
|
(
|
|
"src/agent/generation/canvas_generation.rs",
|
|
&[
|
|
"platform_art_asset_art_spec",
|
|
"build_platform_art_asset_prompt",
|
|
],
|
|
),
|
|
(
|
|
"src/agent/generation/role_briefs.rs",
|
|
&[
|
|
"game_creator_role_agent_system_prompt",
|
|
"render_local_agent_role_brief",
|
|
"local_role_downstream_constraint",
|
|
"local_role_acceptance_risk",
|
|
],
|
|
),
|
|
(
|
|
"../../../server-rs/crates/platform-editor-agent/src/agent/prompt.rs",
|
|
&[
|
|
"editor_agent_system_prompt",
|
|
"edit_image_tool_description",
|
|
"generate_image_tool_description",
|
|
"generate_character_tool_description",
|
|
"generate_icon_spritesheet_tool_description",
|
|
"generate_ui_design_tool_description",
|
|
],
|
|
),
|
|
(
|
|
"../../../server-rs/crates/platform-agent-harness/src/prompt.rs",
|
|
&["build_tools_system_prompt"],
|
|
),
|
|
];
|
|
for (path, functions) in cases {
|
|
let source = read(&root.join(path));
|
|
let (masked, literals) = mask_literals_and_comments(&source);
|
|
for function in *functions {
|
|
let body = function_range(&masked, function);
|
|
for literal in literals
|
|
.iter()
|
|
.filter(|literal| body.contains(&literal.start))
|
|
{
|
|
let diagnostic = (*function == "render_game_creator_codex_cli_prompt"
|
|
&& [
|
|
"\"序列化 Codex CLI Agent 消息失败:{error}\"",
|
|
"\"序列化 Codex CLI Agent 函数目录失败:{error}\"",
|
|
"\"Codex CLI Agent prompt 超过 {} 字节上限\"",
|
|
]
|
|
.contains(&&source[literal.clone()]))
|
|
|| (*function == "build_local_project_prompt_polish_prompt"
|
|
&& &source[literal.clone()] == "\"待润色的内容为空,无法润色\"");
|
|
assert!(
|
|
diagnostic || !is_prose(&source[literal.clone()]),
|
|
"{path}::{function} contains inline prompt prose: {}",
|
|
&source[literal.clone()]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn tool_description_fields_use_external_text_references() {
|
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
let mut paths = vec![
|
|
root.join("src/agent/direct_tools_mcp.rs"),
|
|
root.join("src/agent_native_tools.rs"),
|
|
root.join("src/agent/interaction.rs"),
|
|
];
|
|
for entry in std::fs::read_dir(
|
|
root.join("../../../server-rs/crates/platform-editor-agent/src/agent/tools"),
|
|
)
|
|
.expect("tool sources")
|
|
{
|
|
let path = entry.expect("tool file").path();
|
|
if path.extension().is_some_and(|extension| extension == "rs") {
|
|
paths.push(path);
|
|
}
|
|
}
|
|
for path in paths {
|
|
let source = read(&path);
|
|
let production = source
|
|
.split("\n#[cfg(test)]\nmod tests")
|
|
.next()
|
|
.expect("production source");
|
|
let inline = inline_description_literals(production);
|
|
assert!(
|
|
inline.is_empty(),
|
|
"{} contains inline tool descriptions: {:?}",
|
|
path.display(),
|
|
inline
|
|
.iter()
|
|
.map(|span| &production[span.clone()])
|
|
.collect::<Vec<_>>()
|
|
);
|
|
let (masked, literals) = mask_literals_and_comments(production);
|
|
if masked.contains("fn description(") {
|
|
let body = function_range(&masked, "description");
|
|
for literal in literals
|
|
.iter()
|
|
.filter(|literal| body.contains(&literal.start))
|
|
{
|
|
assert!(
|
|
!is_prose(&production[literal.clone()]),
|
|
"{}::description contains inline prompt prose",
|
|
path.display()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn prompt_and_fallback_constants_reference_external_texts() {
|
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
for (path, names) in [
|
|
(
|
|
"src/agent/direct_runtime/mod.rs",
|
|
vec![
|
|
"DIRECT_TAONIER_IDENTITY_GUIDANCE",
|
|
"DIRECT_AGC_ENGINEERING_GUIDANCE",
|
|
"DIRECT_UNITY_BUILTIN_PLUGIN_GUIDANCE",
|
|
"DIRECT_COCOS_BUILTIN_PLUGIN_GUIDANCE",
|
|
"DIRECT_COCOS_CAPABILITY_GUIDE",
|
|
"DIRECT_ENGINE_FREEDOM_GUIDANCE",
|
|
],
|
|
),
|
|
(
|
|
"src/agent/codex_app_server/mod.rs",
|
|
vec![
|
|
"CODEX_APP_SERVER_BASE_INSTRUCTIONS_FALLBACK",
|
|
"DIRECT_CODEX_BASE_INSTRUCTIONS_FALLBACK",
|
|
],
|
|
),
|
|
(
|
|
"src/agent/runtime_driver.rs",
|
|
vec!["AGENT_RUNTIME_AUTONOMOUS_TOOL_PLAN_PAYLOAD_GUIDANCE"],
|
|
),
|
|
(
|
|
"src/agent/direct_codex_attachments.rs",
|
|
vec!["HOME_ATTACHMENT_HEADER", "PROJECT_ATTACHMENT_HEADER"],
|
|
),
|
|
(
|
|
"src/agent/runtime_actions/provider_request_builders.rs",
|
|
vec!["AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL"],
|
|
),
|
|
] {
|
|
let source = read(&root.join(path));
|
|
let (masked, literals) = mask_literals_and_comments(&source);
|
|
for name in names {
|
|
let start = masked.find(&format!("const {name}:")).expect(name);
|
|
let end = start + masked[start..].find(';').expect("constant end");
|
|
for literal in literals
|
|
.iter()
|
|
.filter(|literal| (start..end).contains(&literal.start))
|
|
{
|
|
assert!(
|
|
!is_prose(&source[literal.clone()]),
|
|
"{path}::{name} contains inline prompt prose"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn inline_description_literals(source: &str) -> Vec<Range<usize>> {
|
|
let (masked, literals) = mask_literals_and_comments(source);
|
|
literals
|
|
.windows(2)
|
|
.filter_map(|pair| {
|
|
if &source[pair[0].clone()] != "\"description\"" {
|
|
return None;
|
|
}
|
|
let expression: String = masked[pair[0].end..pair[1].start]
|
|
.chars()
|
|
.filter(|ch| !ch.is_whitespace())
|
|
.collect();
|
|
(expression.starts_with(':')
|
|
&& !expression.starts_with(":{")
|
|
&& !expression.starts_with(":[")
|
|
&& !expression
|
|
.chars()
|
|
.any(|ch| matches!(ch, ',' | ';' | '}' | ']' | ')'))
|
|
&& !expression.ends_with("prompt_text!(")
|
|
&& !expression.ends_with("include_str!("))
|
|
.then(|| pair[1].clone())
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn tool_description_boundary_covers_multiline_and_formatted_values() {
|
|
for source in [
|
|
"\"description\":\n \"正文\"",
|
|
r#""description": format!("正文 {value}", value=value)"#,
|
|
r##""description": /* context */ concat!(r#"正文"#, "more")"##,
|
|
] {
|
|
assert_eq!(inline_description_literals(source).len(), 1, "{source}");
|
|
}
|
|
for source in [
|
|
r#""description": crate::prompt_text!("tools.description")"#,
|
|
r#""description": format!(include_str!("../prompts/tool.txt"), value=value)"#,
|
|
r#""description": tool_description(), "parameters": {}"#,
|
|
r#""description": { "type": "string", "description": crate::prompt_text!("tools.description") }"#,
|
|
] {
|
|
assert!(inline_description_literals(source).is_empty(), "{source}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn source_boundary_scanner_distinguishes_comments_raw_strings_and_format_braces() {
|
|
let source = r##"// fn sample() { "注释" }
|
|
fn sample() { let a = r#"正文 { brace }"#; let b = "escaped \"quote\""; let quote = '"'; }
|
|
"##;
|
|
let (masked, literals) = mask_literals_and_comments(source);
|
|
let body = function_range(&masked, "sample");
|
|
let found: Vec<_> = literals
|
|
.iter()
|
|
.filter(|literal| body.contains(&literal.start))
|
|
.collect();
|
|
assert_eq!(found.len(), 2);
|
|
assert!(is_prose(&source[found[0].clone()]));
|
|
assert!(!is_prose(&source[found[1].clone()]));
|
|
}
|