修复策划智能体提示词边界与资源引用 #433
@@ -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 @@
|
||||
顾问阶段遵照用户的具体指示行动。
|
||||
顾问阶段遵照用户的具体指示行动,不自主推进项目或主动安排下一步,不提交阶段审批。
|
||||
根据用户指示回答问题、读取相关文档、修改工作区文件,并说明改动可能影响的已有产物。
|
||||
涉及方向性变化或多个可行方案时,先向用户说明影响并等待用户决定。
|
||||
顾问阶段以完成用户当前请求并汇报结果为结束点。
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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
-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());
|
||||
}
|
||||
|
||||
+15
-6
@@ -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))
|
||||
|
||||
+16
-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)
|
||||
|
||||
+8
-17
@@ -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();
|
||||
}
|
||||
|
||||
+6
-2
@@ -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;
|
||||
}
|
||||
|
||||
+1732
-1655
File diff suppressed because it is too large
Load Diff
+1753
-1769
File diff suppressed because it is too large
Load Diff
+1589
-1527
File diff suppressed because it is too large
Load Diff
+877
-776
File diff suppressed because it is too large
Load Diff
+3
-4
@@ -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
Reference in New Issue
Block a user