修复策划智能体提示词边界与资源引用 #433

Merged
lhk229 merged 6 commits from codex/fix-design-agent-prompt-boundaries into master 2026-09-20 18:07:38 +08:00
28 changed files with 6166 additions and 6030 deletions
@@ -312,34 +312,15 @@ function runShard(executable, shardIndex, shardCount, shardTestNames) {
},
);
const failureLines = [];
let inFailureList = false;
// Rust 的首个 failures: 后有空行,不能按空行结束采集,否则会丢掉 panic 详情。
// 只保存有界尾部;成功时不输出,失败时优先输出完整失败段。
let stdoutTail = '';
let stderr = '';
const consumeLine = (rawLine) => {
const line = rawLine.replace(/\r$/, '');
if (line.includes('failures:')) {
inFailureList = true;
return;
}
if (inFailureList) {
if (line.trim().length === 0) {
inFailureList = false;
return;
}
failureLines.push(line.trim());
}
};
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
let stdoutBuffer = '';
child.stdout.on('data', (chunk) => {
stdoutBuffer += chunk;
const lines = stdoutBuffer.split('\n');
stdoutBuffer = lines.pop() ?? '';
for (const line of lines) {
consumeLine(line);
}
stdoutTail = (stdoutTail + chunk).slice(-64_000);
});
child.stderr.on('data', (chunk) => {
stderr += chunk;
@@ -356,12 +337,17 @@ function runShard(executable, shardIndex, shardCount, shardTestNames) {
});
});
child.on('close', (code) => {
const output = stdoutTail.replace(/\r\n/g, '\n');
const failureStart = output.indexOf('failures:\n');
resolve({
label,
ok: code === 0,
durationMs: Date.now() - startedAt,
testCount: shardTestNames.length,
failures: failureLines,
failures: output
.slice(failureStart < 0 ? 0 : failureStart)
.trim()
.split('\n'),
stderr,
});
});
@@ -438,7 +424,7 @@ async function main() {
}
failed = true;
console.error(
`[rust-shards] ${result.label} FAILED: ${result.testCount} test(s) in ${formatDuration(result.durationMs)}`,
`[rust-shards] ${result.label} FAILED (selected ${result.testCount} test(s)) in ${formatDuration(result.durationMs)}`,
);
for (const failure of result.failures) {
console.error(`[rust-shards] ${failure}`);
@@ -0,0 +1,70 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const runner = fileURLToPath(
new URL('./run-rust-shell-test-shards.mjs', import.meta.url),
);
function runFixture(t, source) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-shard-output-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
fs.mkdirSync(path.join(root, 'src'));
fs.writeFileSync(
path.join(root, 'Cargo.toml'),
'[package]\nname = "shard-output-fixture"\nversion = "0.1.0"\nedition = "2021"\n',
);
fs.writeFileSync(path.join(root, 'src/lib.rs'), source);
const result = spawnSync(
process.execPath,
[
runner,
`--manifest=${path.join(root, 'Cargo.toml')}`,
'--target-kind=lib',
'--no-locked',
'--shards=1',
`--shard-tmp-root=${path.join(root, 'tmp')}`,
],
{
encoding: 'utf8',
timeout: 60_000,
windowsHide: true,
env: { ...process.env, CARGO_TARGET_DIR: path.join(root, 'target') },
},
);
assert.ifError(result.error);
return { status: result.status, output: result.stdout + result.stderr };
}
test('failed shard retains panic details and separates selected count from failures', (t) => {
const result = runFixture(
t,
`
#[test]
fn passing_case() {}
#[test]
fn failing_case() {
assert_eq!(1, 2, "shard panic evidence");
}
`,
);
assert.equal(result.status, 1, result.output);
assert.match(result.output, /FAILED \(selected 2 test\(s\)\)/);
assert.match(result.output, /failing_case/);
assert.match(result.output, /panicked at src[\\/]lib\.rs:/);
assert.match(result.output, /shard panic evidence/);
assert.match(result.output, /left: 1/);
assert.match(result.output, /right: 2/);
assert.match(result.output, /1 passed; 1 failed/);
});
test('successful shard keeps its compact summary', (t) => {
const result = runFixture(t, '#[test]\nfn passing_case() {}\n');
assert.equal(result.status, 0, result.output);
assert.match(result.output, /shard 1\/1 ok: 1 test\(s\)/);
assert.doesNotMatch(result.output, /test passing_case \.\.\. ok/);
});
@@ -1,4 +1,3 @@
顾问阶段遵照用户的具体指示行动。
顾问阶段遵照用户的具体指示行动,不自主推进项目或主动安排下一步,不提交阶段审批
根据用户指示回答问题、读取相关文档、修改工作区文件,并说明改动可能影响的已有产物。
涉及方向性变化或多个可行方案时,先向用户说明影响并等待用户决定。
顾问阶段以完成用户当前请求并汇报结果为结束点。
@@ -3,7 +3,7 @@
版本:v3 | 规则:台账放活队列——design 只放结论、分析只放论证、决定与开放问题住这里。编号连续不复用;被推翻的行标 overturned 挂新行,不删行。
状态六态:`confirmed`(用户亲口/亲选)/ `auto_decided`(技术类代决,必带理由+推翻条件,用户一键可翻)/ `default_pending`(默认建议兜底,用户未点头)/ `prototype_pending`(待原型验证)/ `pending_user`(等用户拍板)/ `overturned`(被推翻,挂旧行编号)。
> 编号口径:D-01~D-13 与 exemplars/stardew-analysis.md 台账节选一致(D-04~D-06、D-08~D-10、D-12 原为"就地小权衡,直接登记未开条目",此处按登记口径展开);D-14 起为技术文档期新增,与 stardew-tdd-tech.md 开放问题回执互引。
> 编号口径:D-01~D-13 与 templates/stardew-analysis.md 台账节选一致(D-04~D-06、D-08~D-10、D-12 原为"就地小权衡,直接登记未开条目",此处按登记口径展开);D-14 起为技术文档期新增,与 stardew-tdd-tech.md 开放问题回执互引。
## 当前待办(活队列)
@@ -1,7 +1,7 @@
# 顶层设计:《星露谷物语》
## 顶层定位与规模锚点
顶层设计让玩家每天都在想:
顶层不是做长线农场生产线,也不是做以探索战斗为主的活动清单,而是让玩家每天都在想:
> "今天做什么?——下雨天不用浇水,正好下矿井;回来的路上把罗宾的生日礼物送了。"
| 项 | 定义 |
@@ -5,7 +5,7 @@ name: game-gdd-architecture
description: 写游戏策划案(GDD)系统架构时使用。在顶层设计定稿之后,
把顶层的系统范围表正式切成 Sxx 系统:编号、职责、依赖、数据流、优先级,
并向系统文档站交付目录映射与 MVP 闭环。配套:templates/architecture.md、
templates/analysis.md(全局一份)、exemplars/stardew-architecture.md、exemplars/stardew-analysis.md(全局一份)。
templates/analysis.md(全局一份)、exemplars/stardew-architecture.md、templates/stardew-analysis.md(全局一份)。
---
# 系统架构写法(策划 agent · 系统架构分册)
@@ -5,7 +5,7 @@ name: game-gdd-concept
description: 写游戏策划案(GDD)概念层时使用。把一句话游戏想法写成一份
"一次写对、之后不动"的立项概念文档——它是后续所有设计争议的仲裁依据。
任何游戏类型通用。配套:templates/concept-design.md、templates/analysis.md(全局一份)、
exemplars/stardew-concept.md、exemplars/stardew-analysis.md(全局一份)。
exemplars/stardew-concept.md、templates/stardew-analysis.md(全局一份)。
---
# 概念层写法(策划 agent · 概念层分册)
@@ -5,7 +5,7 @@ name: game-gdd-top-design
description: 写游戏策划案(GDD)顶层设计时使用。在概念层定稿之后,
回答"玩家为什么一直玩"——把概念变成可玩的时间结构(循环/资源/取舍/节奏),
并向架构层交付系统范围。配套:templates/top-design.md、templates/analysis.md(全局一份)、
exemplars/stardew-top-design.md、exemplars/stardew-analysis.md(全局一份)。
exemplars/stardew-top-design.md、templates/stardew-analysis.md(全局一份)。
---
# 顶层设计写法(策划 agent · 顶层设计分册)
@@ -45,7 +45,7 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
| # | 节 | 是什么 | 为什么写 | 和谁咬合 |
|---|---|---|---|---|
| 1 | 顶层定位与规模锚点 | 承概念定稿 + "让玩家每天都在想"念头句 + 规模参数表(循环单位/段落/复杂度/长期主轴) | 循环单位定错全盘错;定位句防止顶层漂离概念 | 承概念层"概念定稿";念头句是概念层玩家念头的时间维度版 |
| 1 | 顶层定位与规模锚点 | 承概念定稿 + 按需说明易混淆方向及排除理由 + "让玩家每天都在想"念头句 + 规模参数表(循环单位/段落/复杂度/长期主轴) | 循环单位定错全盘错;定位句防止顶层漂离概念 | 承概念层"概念定稿";念头句是概念层玩家念头的时间维度版 |
| 2 | 设计目标 | 几种回报、如何互相供给 | 回报并列=小游戏拼盘;互相供给才是循环 | 供给关系落到 4~5 的循环里 |
| 3 | 核心推动力 | 按项目实际存在的即时、阶段或长期推动力组织 | 玩家"什么时候被什么推着走"的推动结构 | 与实际节奏结构对应 |
| 4 | 大循环 | 跨较长时间的循环:文字箭头 + 核心循环图 | 长期留存的结构骨架 | 与 5、7 三层互检:大循环的每环应有小循环供血 |
@@ -88,6 +88,7 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/top-design.md
### 1. 顶层定位与规模锚点
承接概念定稿说明核心定位;存在容易混淆的方向时,说明排除方向及理由,表述按项目需要组织。
顶层设计让玩家每天都在想:
> "__(玩家每天惦记的那件事)"
规模锚点表:循环单位 / 段落构成 / 操作复杂度 / 经营复杂度 / 长期主轴排序。
@@ -1,4 +1,4 @@
### C1 例子_星露谷_分析.md(分析金样;→ exemplars/stardew-analysis.md
### C1 例子_星露谷_分析.md(分析金样;→ templates/stardew-analysis.md
# 分析:《星露谷物语》
@@ -5,6 +5,9 @@
# 顶层设计:《游戏名》
## 顶层定位与规模锚点
核心定位:__。
容易混淆的方向及排除理由(按需):__。
顶层设计让玩家每天都在想:
> "__"
@@ -7,12 +7,12 @@
正式策划文档在文档头部写明版本标记,例如“版本:v1”。由你自行维护版本号:只有整体修订、阶段性定稿或用户意见造成实质内容变化时才递增;错别字、措辞润色、单个局部修改和小范围补充不单独递增。
阶段审批是每个阶段的最终检查,将已完成的本阶段产物交给用户检阅。提交前,解决所有影响本阶段完成的关键问题,或明确说明它们不阻塞本阶段交付,并更新相关产物。可以保留不阻塞当前阶段的后续事项和待原型验证项。
阶段审批是五个策划阶段各自的最终检查,将已完成的本阶段产物交给用户检阅。需要用户选择的关键问题先通过问询解决,阶段审批不承担问询功能。提交前,解决所有影响本阶段完成的关键问题,或明确说明它们不阻塞本阶段交付,并更新相关产物。可以保留不阻塞当前阶段的后续事项和待原型验证项。
过程文档用于记录关键依据、决定和待办。阶段内优先完成主要设计内容;只有稳定且影响后续工作的决定才需要同步到多个过程文档。阶段提交前,补齐影响验收的关键记录。
过程文档用于记录关键依据、决定和待办,不要求实时完整,也不应重复正式设计文档。阶段内优先完成主要设计内容;只有稳定且影响后续工作的决定才需要同步到多个过程文档。阶段提交前,补齐影响验收的关键记录。
阶段获批后,产物中已经采用的方案作为后续工作的依据,并保留原有决策来源。用户主动质疑或出现新的约束冲突时,再重新讨论相关决定。
用户说“继续”时,继续推进当前阶段最有价值的工作。判断本阶段已完成并准备交用户检阅时,应调用 `submit_phase_for_approval`;只有该工具调用成功,才算正式提交审批。
用户说“继续”时,继续推进当前阶段最有价值的工作。在五个策划阶段中,判断本阶段已完成并准备交用户检阅时,应调用 `submit_phase_for_approval`;只有该工具调用成功,才算正式提交审批。
用户口头表示已经批准或要求进入下一阶段时,先调用 `get_workflow_status` 确认 Runtime 当前阶段。只有用户批准正式审批请求后,Runtime 才会推进阶段;审批工具是推进阶段的唯一方式。
@@ -9,5 +9,5 @@
{"type":"function","function":{"name":"write_file","description":"创建或覆盖工作目录内的 UTF-8 文本文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"],"additionalProperties":false}}},
{"type":"function","function":{"name":"search_text","description":"在工作目录内搜索文本。","parameters":{"type":"object","properties":{"query":{"type":"string"},"path":{"type":"string"}},"required":["query"],"additionalProperties":false}}},
{"type":"function","function":{"name":"ask_clarification","description":"向用户展示多选项问询澄清卡片,选项数2-4。多选一场景时优先使用本工具,其他场景可以纯文本进行问询。每轮最多调用一次。","parameters":{"type":"object","properties":{"question":{"type":"string"},"options":{"type":"array","items":{"type":"string"}}},"required":["question"],"additionalProperties":false}}},
{"type":"function","function":{"name":"submit_phase_for_approval","description":"提交当前策划阶段供用户审批。当你判断当前阶段已经完成并准备交用户检阅时必须调用。用户批准后 Runtime 自动进入下一阶段。","parameters":{"type":"object","properties":{},"additionalProperties":false}}}
{"type":"function","function":{"name":"submit_phase_for_approval","description":"提交五个策划阶段中的当前阶段供用户审批。当你判断当前阶段已经完成并准备交用户检阅时必须调用。用户批准后 Runtime 自动进入下一阶段。","parameters":{"type":"object","properties":{},"additionalProperties":false}}}
]
@@ -5248,10 +5248,17 @@ mod tests {
#[test]
fn direct_codex_turn_keeps_system_prompt_out_of_user_input() {
let request = LlmRunRequest::single_turn("AGC 系统规则", "制作一个可玩的游戏");
assert_eq!(direct_codex_base_instructions(&request), "AGC 系统规则");
assert_eq!(direct_codex_user_prompt(&request), "制作一个可玩的游戏");
assert!(!direct_codex_user_prompt(&request).contains("AGC 系统规则"));
let system =
crate::agent::direct_runtime::build_direct_codex_system_prompt_with_creation_type(
Path::new("."),
Some("art"),
)
.expect("creation context");
for prompt in ["你好", "今天多少号?", "画一个橙色陶罐角色"] {
let request = LlmRunRequest::single_turn(system.clone(), prompt);
assert_eq!(direct_codex_base_instructions(&request), system);
assert_eq!(direct_codex_user_prompt(&request), prompt);
}
}
#[tokio::test]
@@ -5287,67 +5287,6 @@ mod direct_turn_stream_writer_tests {
}
}
/// Default product path: one user message becomes one turn on the same
/// project-bound Codex app-server thread. The client does not classify the
/// intent or perform hidden art, preview, repair, or another LLM workflow. If
/// Codex actually changes the canonical game files, the client performs only
/// deterministic resource/version projection so its own workspace reflects
/// the files now on disk.
async fn run_direct_game_creator_turn_with<F, Fut>(
root: &Path,
prompt: &str,
run_turn: F,
) -> Result<String, DirectCodexTurnFailure>
where
F: FnOnce(String, String) -> Fut,
Fut: Future<Output = Result<String, String>>,
{
run_direct_game_creator_turn_with_creation_type(root, prompt, None, run_turn).await
}
async fn run_direct_game_creator_turn_with_creation_type<F, Fut>(
root: &Path,
prompt: &str,
creation_type: Option<&str>,
run_turn: F,
) -> Result<String, DirectCodexTurnFailure>
where
F: FnOnce(String, String) -> Fut,
Fut: Future<Output = Result<String, String>>,
{
emit_direct_game_creator_progress(root, "codex.turn", "陶泥儿正在处理这条消息");
let previous_output_fingerprint = direct_codex_output_fingerprint(root);
let base_system_prompt =
build_direct_codex_system_prompt_with_creation_type(root, creation_type).map_err(
|error| DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error),
)?;
let engine_contract = direct_engine_three_dimensional_contract(root, prompt);
let system_prompt = match engine_contract.as_deref() {
Some(contract) => format!("{contract}\n{base_system_prompt}")
.chars()
.take(MAX_DIRECT_SYSTEM_PROMPT_CHARS)
.collect(),
None => base_system_prompt,
};
let reply = run_turn(system_prompt, prompt.to_string())
.await
.map_err(|error| {
DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error)
})?;
if direct_codex_output_fingerprint(root) != previous_output_fingerprint {
emit_direct_game_creator_progress(
root,
"project.sync",
"检测到游戏文件更新,正在同步客户端资源",
);
sync_direct_codex_project_file_projection_at(root, Some(&previous_output_fingerprint))
.map_err(|error| {
DirectCodexTurnFailure::new(DirectCodexFailureStage::VersionRegistration, error)
})?;
}
Ok(reply)
}
#[allow(dead_code)]
async fn run_direct_game_creator_turn_with_private_editor_credentials(
root: &Path,
@@ -6482,159 +6421,27 @@ mod tests {
.is_err());
}
#[tokio::test]
async fn direct_creation_type_keeps_the_original_user_prompt_unchanged() {
let root = tempfile::tempdir().expect("temp dir");
init_local_game_project_at(root.path(), "direct-art-context", "素材上下文测试")
.expect("init project");
let reply = run_direct_game_creator_turn_with_creation_type(
root.path(),
"画一个橙色陶罐角色",
Some("art"),
|system, prompt| async move {
assert!(system.contains("art / 做素材"));
assert_eq!(prompt, "画一个橙色陶罐角色");
assert!(!prompt.contains("初始意图"));
Ok("已理解素材需求。".to_string())
},
)
.await
.expect("direct creation type turn");
assert_eq!(reply, "已理解素材需求。");
}
#[tokio::test]
async fn default_direct_turn_forwards_a_greeting_without_client_generation_workflow() {
let root = tempfile::tempdir().expect("temp dir");
init_local_game_project_at(root.path(), "direct-chat-only", "纯对话测试")
.expect("init project");
std::fs::write(
root.path().join("game/index.html"),
"<!doctype html><title>before</title>",
)
.expect("index");
std::fs::write(root.path().join("game/style.css"), "body { color: black; }")
.expect("style");
std::fs::write(root.path().join("game/game.js"), "console.info('before');")
.expect("script");
let manifest_before =
std::fs::read(root.path().join(".agent/manifest.json")).expect("manifest before");
let index_before =
std::fs::read(root.path().join("game/index.html")).expect("index before");
let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let captured = std::sync::Arc::clone(&seen);
let reply =
run_direct_game_creator_turn_with(root.path(), "你好", move |system, prompt| {
let captured = std::sync::Arc::clone(&captured);
async move {
captured
.lock()
.expect("capture direct handoff")
.push((system, prompt));
Ok("你好!我是陶泥儿。".to_string())
}
})
.await
.expect("greeting reply");
assert_eq!(reply, "你好!我是陶泥儿。");
let calls = seen.lock().expect("read direct handoff");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].1, "你好");
assert!(calls[0].0.contains("普通对话"));
assert!(calls[0].0.contains("不触碰工作区"));
assert_eq!(
std::fs::read(root.path().join("game/index.html")).expect("index after"),
index_before
);
assert_eq!(
std::fs::read(root.path().join(".agent/manifest.json")).expect("manifest after"),
manifest_before
);
assert!(
!root
.path()
.join(".agent/runtime/direct-codex-browser-validation")
.exists(),
"ordinary chat must not start client browser validation"
);
}
#[tokio::test]
async fn default_direct_turn_forwards_a_date_question_without_client_art_or_version_side_effects(
) {
let root = tempfile::tempdir().expect("temp dir");
init_local_game_project_at(root.path(), "direct-date-only", "日期对话测试")
.expect("init project");
let manifest_before =
std::fs::read(root.path().join(".agent/manifest.json")).expect("manifest before");
let reply = run_direct_game_creator_turn_with(
root.path(),
"今天多少号?",
|system, prompt| async move {
assert!(system.contains("普通对话"));
assert!(system.contains("不触碰工作区"));
assert_eq!(prompt, "今天多少号?");
Ok("今天是测试日期。".to_string())
},
)
.await
.expect("date reply");
assert_eq!(reply, "今天是测试日期。");
assert_eq!(
std::fs::read(root.path().join(".agent/manifest.json")).expect("manifest after"),
manifest_before
);
assert!(
!root.path().join("assets/art-spec.png").exists()
&& !root
.path()
.join("assets/direct-game-background.png")
.exists()
&& !root.path().join("assets/art-spritesheet.png").exists(),
"ordinary chat must not prepare a TaoNier art package"
);
}
#[tokio::test]
async fn default_direct_turn_projects_changed_game_files_without_supervisor_or_art() {
#[test]
fn direct_file_projection_registers_changes_without_art_and_preserves_unchanged_versions() {
let root = tempfile::tempdir().expect("temp dir");
init_local_game_project_at(root.path(), "direct-file-projection", "文件投影测试")
.expect("init project");
let write_root = root.path().to_path_buf();
let before = direct_codex_output_fingerprint(root.path());
let files = [
("game/index.html", "<!doctype html><canvas></canvas>"),
("game/style.css", "canvas { background: red; }"),
("game/game.js", "requestAnimationFrame(() => {});"),
];
for (path, content) in files {
std::fs::write(root.path().join(path), content).expect("game file");
}
let reply = run_direct_game_creator_turn_with(
root.path(),
"创建一个纯色方块游戏",
move |system, prompt| {
let write_root = write_root.clone();
async move {
assert!(system.contains("唯一执行主体"));
assert_eq!(prompt, "创建一个纯色方块游戏");
std::fs::write(
write_root.join("game/index.html"),
"<!doctype html><link rel=\"stylesheet\" href=\"style.css\"><canvas></canvas><script src=\"game.js\"></script>",
)
.expect("index");
std::fs::write(write_root.join("game/style.css"), "canvas { background: red; }")
.expect("style");
std::fs::write(write_root.join("game/game.js"), "requestAnimationFrame(() => {});")
.expect("script");
Ok("已写入三个游戏文件。".to_string())
}
},
)
.await
.expect("direct file projection");
sync_direct_codex_project_file_projection_at(root.path(), Some(&before))
.expect("project changed files");
assert_eq!(reply, "已写入三个游戏文件。");
let manifest =
read_manifest(&root.path().join(".agent/manifest.json")).expect("projected manifest");
for (local_path, kind, media_type) in direct_codex_game_outputs(&root.path()) {
for (local_path, kind, media_type) in direct_codex_game_outputs(root.path()) {
assert!(manifest.assets.iter().any(|asset| {
asset.local_path == local_path
&& asset.kind == kind
@@ -6651,6 +6458,28 @@ mod tests {
.map(|task| task.status.clone()),
Some(GameCreationAppTaskStatus::Completed)
);
let revision = read_game_creator_agent_runtime_project_revision(root.path())
.expect("project revision");
let unchanged = direct_codex_output_fingerprint(root.path());
sync_direct_codex_project_file_projection_at(root.path(), Some(&unchanged))
.expect("project unchanged files");
let after =
read_manifest(&root.path().join(".agent/manifest.json")).expect("unchanged manifest");
assert_eq!(after.assets, manifest.assets);
assert_eq!(after.versions, manifest.versions);
assert_eq!(
read_game_creator_agent_runtime_project_revision(root.path())
.expect("unchanged revision")
.revision,
revision.revision
);
for (path, content) in files {
assert_eq!(
std::fs::read_to_string(root.path().join(path))
.expect("game file after projection"),
content
);
}
assert!(!root.path().join("assets/art-spec.png").exists());
assert!(!root.path().join(".agent/runtime/runtimes").exists());
}
@@ -1338,16 +1338,25 @@ mod supervisor_collaboration_repair_tests {
#[test]
fn collaboration_repair_instruction_composes_the_generated_fragment() {
let protocol_error = "missing collaboration";
let fragment = required_runtime_prompt_section(
RUNTIME_PROMPT_PROVIDER_AUTONOMOUS_INITIAL_COLLABORATION_REPAIR_SECTION,
)
.trim();
let instruction = provider_collaboration_repair_instruction(
"missing collaboration",
protocol_error,
RUNTIME_PROMPT_PROVIDER_AUTONOMOUS_INITIAL_COLLABORATION_REPAIR_SECTION,
);
assert!(instruction.starts_with(
"上一条输出不符合工具计划协议:missing collaboration\n本片段适用于 GUI / CLI"
));
assert!(instruction.contains("本次修复原生工具目录"));
assert_eq!(instruction.matches("一次性建立完整首批合同").count(), 1);
assert_eq!(
instruction,
format!(
prompt_text!("recovery.collaboration_protocol"),
fragment,
protocol_error = protocol_error
)
);
assert_eq!(instruction.matches(fragment).count(), 1);
assert!(instruction.contains("design-director"));
assert!(instruction.contains("art-director"));
assert!(instruction.contains("code-director"));
@@ -798,25 +798,6 @@ fn provider_transient_retry_uses_configured_max_retries_for_every_run_profile()
fs::remove_dir_all(root).ok();
}
#[test]
fn autonomous_game_build_tool_plan_guidance_bounds_each_source_payload() {
let guidance = AGENT_RUNTIME_AUTONOMOUS_TOOL_PLAN_PAYLOAD_GUIDANCE;
for expected in [
"最多提交一个",
"不得超过 8000 字符",
"合计不得超过 10000 字符",
"可运行且保留扩展点的紧凑 scaffold",
"后续 planning 轮次",
"闭合的 <script> 和 </html>",
"不得在一个 function arguments 中输出完整大型游戏",
] {
assert!(
guidance.contains(expected),
"autonomous payload guidance missing {expected}"
);
}
}
#[test]
fn autonomous_game_build_tool_plan_payload_is_enforced_before_actions() {
let plan_with = |actions| AgentRuntimeToolPlan {
@@ -5403,7 +5384,6 @@ async fn background_agent_runtime_repairs_malformed_native_function_arguments()
assert!(initial_request.contains("必须直接调用当前请求提供的原生函数"));
assert!(!initial_request.contains("Legacy text JSON schema"));
assert!(initial_request.contains("arguments.input"));
assert!(initial_request.contains("禁止把 input 扁平到 arguments 顶层"));
assert!(initial_request.contains("必须调用 respond_to_user"));
let repair_request = receiver
.recv_timeout(Duration::from_secs(2))
@@ -210,9 +210,23 @@ async fn background_agent_runtime_auto_compacts_before_over_budget_planning() {
.recv_timeout(Duration::from_secs(4))
.expect("post-compaction planning Provider request");
assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err());
assert!(compaction_request.contains("你负责压缩 Agent 的旧历史"));
assert!(compaction_request.contains("AUTO_CONTEXT_MARKER_0"));
let compaction_json = mock_http_request_json(&compaction_request);
let compaction_system = compaction_json["input"]
.as_array()
.expect("compaction responses input")
.iter()
.find(|message| message["role"] == "system")
.and_then(|message| message["content"].as_array())
.and_then(|content| content.iter().find(|part| part["type"] == "input_text"))
.and_then(|part| part["text"].as_str())
.expect("compaction system input text");
assert_eq!(
compaction_system,
prompt_text!("interaction.compaction_system")
);
assert!(compaction_json["input"]
.to_string()
.contains("AUTO_CONTEXT_MARKER_0"));
assert!(compaction_json
.get("tools")
.and_then(Value::as_array)
@@ -1,20 +1,11 @@
import { it } from '../harness';
import { openPreviewShortcutProject } from './preview-shortcuts/actions';
import { arrangePreviewShortcutTest } from './preview-shortcuts/arrange';
import { assertPlanningAndStatusShortcutFlow } from './preview-shortcuts/assert-planning-and-status-shortcuts';
import { assertPlaytestAndReleaseShortcutFlow } from './preview-shortcuts/assert-playtest-and-release-shortcuts';
import { assertProjectAndDesignShortcutFlow } from './preview-shortcuts/assert-project-and-design-shortcuts';
import { assertProjectToolsAndPreviewFlow } from './preview-shortcuts/assert-project-tools-and-preview';
import { registerPlanningAndStatusShortcutTests } from './preview-shortcuts/assert-planning-and-status-shortcuts';
import { registerPlaytestAndReleaseShortcutTests } from './preview-shortcuts/assert-playtest-and-release-shortcuts';
import { registerProjectAndDesignShortcutTests } from './preview-shortcuts/assert-project-and-design-shortcuts';
import { registerProjectToolsAndPreviewTests } from './preview-shortcuts/assert-project-tools-and-preview';
export function registerProjectPreviewShortcutTests() {
it('runs preview shortcuts from the main project window', async () => {
const { invoke } = arrangePreviewShortcutTest();
openPreviewShortcutProject();
await assertProjectAndDesignShortcutFlow(invoke);
await assertPlanningAndStatusShortcutFlow(invoke);
await assertPlaytestAndReleaseShortcutFlow(invoke);
await assertProjectToolsAndPreviewFlow(invoke);
}, 60000);
registerProjectAndDesignShortcutTests();
registerPlanningAndStatusShortcutTests();
registerPlaytestAndReleaseShortcutTests();
registerProjectToolsAndPreviewTests();
}
@@ -1,5 +1,9 @@
import { renderAppAt } from '../../harness';
import { renderAppAt, screen } from '../../harness';
import { arrangePreviewShortcutTest } from './arrange';
export function openPreviewShortcutProject() {
export async function openPreviewShortcutProject() {
const { invoke } = arrangePreviewShortcutTest();
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('已打开:authorized-game');
return invoke;
}
@@ -7,6 +7,9 @@ export function createPreviewShortcutInvoke({
}: PreviewShortcutFixture) {
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'set_agc_plugin_project_path') {
return null;
}
if (command === 'append_local_permission_log') {
return {};
}
@@ -296,7 +299,3 @@ export function createPreviewShortcutInvoke({
return invoke;
}
export type PreviewShortcutInvoke = ReturnType<
typeof createPreviewShortcutInvoke
>;
@@ -55,6 +55,12 @@
提示词外置变更运行 `runtime_prompt_bundle_build``prompt_source_boundaries` 两个 Rust 集成测试,验证编译期文本、目录登记和源码边界;现有 `agc-rust-shard-1` 本地/CI 入口先执行这组检查,再运行分片单测。
提示词测试验证实际请求中的片段来源、动态参数和工具结构;措辞不作为逐字契约。已有行为测试覆盖的限制不再另设整段文案检查。Direct 回合测试复用生产的消息转换和文件投影函数,不维护仅供测试调用的回合编排副本。
AGC 预览快捷操作的界面测试按独立命令或有状态短流程注册,每例重新建立 fixture、原生调用 mock 和页面,并等待项目打开后再记录调用计数。预览启停、导出确认与取消等连续行为保留在同一用例;互不依赖的只读命令不串成一条长对话,也不共享 DOM 或提高超时来容纳整组流程。
Rust 分片失败日志保留有界的失败详情,包括 panic 位置、断言和最终通过/失败数量;分片选中数量标为 selected,避免误读为失败数量。修改分片日志时运行 `node --test apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.test.mjs`,用最小 Rust fixture 验证失败详情和成功摘要。
AGC 运行时配置默认值调整时,同步核对 Rust 默认值、分发配置模板、设置弹窗默认草稿和 `runtime-settings.suite.ts` 的恢复默认断言;显式传入旧值的配置读取用例仍验证原值保留,不批量替换测试数据。
AGC 测试构造单 HTML 项目时,必须在初始化之前写入 HTML,避免自动建立 npm 工程;npm 预览和导出测试应提供 dist 产物。已有图片生成 pending/operation 属于持久化恢复合同,修改工具默认参数后仍须验证旧动作恢复不重复提交、不因默认值变化被误判为新意图。

Some files were not shown because too many files have changed in this diff Show More