合并 master 到 fix/agc-canvas-reference-insert:按新测试替身与类型门禁解冲突
master 侧(PR #601)新增 tests/tauriRuntimeFake.ts(全仓唯一 window.__TAURI__ 安装点)与 tsconfig.tests.json,测试统一改用 installTauriRuntime / resetTauriRuntime,并新增 check:tests:types 门禁 冲突三处测试文件(resourceCanvasChatReferenceDrop.test.tsx、appSurface/project-development.suite.ts、appSurface/design-agent.suite.ts)按语义两边保留:采用 master 的替身安装点与类型收窄(ResourceReference 过滤),保留本分支的真实 DirectProject 聊天面挂载与端到端草稿断言 App 的 props 类型未导出,新增的两处 chat 节点改用 AppComponent 组件类型别名(与 design-agent.suite.ts 同一写法),通过新的 check:tests:types docs 侧 pitfalls.md / decision-log.md 自动合并:本分支的 issue #602 条目与 master 的「根因 7/8」条目都保留 未在本分支修 master 既有红(check:nginx-spa-routes 缺 /pay 与 /profile/payment、external_mcp::semantic 32 vs 30、http_tracing 偶发、AGC Rust shard 4/4),由上游 fix/ci-master-red 处理
This commit is contained in:
@@ -19,7 +19,8 @@
|
||||
"bundled-resources:test": "node --test scripts/prepare-bundled-resources.test.mjs",
|
||||
"llm-status": "node scripts/run-cli-with-config.mjs --llm-status",
|
||||
"config": "node scripts/game-creator-config-wizard.mjs",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && npm run bundled-resources:check && node scripts/check-config.mjs"
|
||||
"check:tests:types": "tsc -p tsconfig.tests.json --noEmit",
|
||||
"typecheck": "npm run check:tests:types && tsc -p tsconfig.json --noEmit && npm run skill-pack:check && npm run bundled-resources:check && node scripts/check-config.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.285",
|
||||
|
||||
@@ -17,8 +17,14 @@
|
||||
"browser.status": "整体 passed={}",
|
||||
"browser.notStarted": "本次未启动 Chromium",
|
||||
"browser.evidence": "[AGC 浏览器事实证据]\nattempt={attempt}; completionError={completion_status}; browser={browser_status}; codeFingerprintChanged={output_changed}\n{}\n诊断与平台素材运行时观察:{}\n交互探针:{}\n客户端已保存结构化证据;它是事实输入,不代表 Codex 已阅读截图或已经完成修复。当前 cwd 是项目根目录;按需读取实际文件,并自行决定是否修改、再次试玩或直接回复。AGC 只负责启动浏览器、采集证据和执行项目边界,Codex 负责解释结果。",
|
||||
"browser.noFailureDetails": "无额外硬失败详情",
|
||||
"browser.noVisibleControls": "未找到可执行的可见控件",
|
||||
"browser.noFailureDetails": "无额外视觉检查失败详情",
|
||||
"browser.noVisibleControls": "无建议性交互探针记录",
|
||||
"browser.gameplayNotRun": "玩法检查:未执行(visual 仅提供视觉证据)。",
|
||||
"browser.gameplayMissing": "固定场景玩法结果缺失,不能确认通过。",
|
||||
"browser.gameplayNoDiagnostic": "无诊断记录",
|
||||
"browser.gameplayNoUnmetAssertion": "无",
|
||||
"browser.gameplayViewport": "{viewport}: gameplayPassed={passed};首条玩法诊断:{diagnostic};首个未通过断言:{assertion}",
|
||||
"browser.gameplayEvidence": "[固定场景玩法证据]\n{}\n断言 passed=false 表示未通过,也可能因前序阻断而未执行;修复以具体诊断为准,不把所有未通过项视为独立故障。固定场景通过不代表完整关卡已验证;reportPath 仅为宿主证据引用,无需读取私有报告。",
|
||||
"system.role": "你是陶泥儿,是 Genarrative 面向用户的游戏创作助手,负责当前任务的执行。先理解用户意图:普通对话直接回答,项目请求按需要检查、修改、运行和验证,并用简洁中文报告真实结果。",
|
||||
"system.workspaceBoundary": "工作区边界:只在当前项目目录内工作;不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径。遇到阻断必须说明具体原因、文件和下一步,不要声称未验证的成功。",
|
||||
"system.toolAuthorization": "AGC 工具授权:agc_tools 使用当前登录会话。工具返回 401/403 时,报告登录或权限状态异常并停止,交由用户处理登录和权限。",
|
||||
|
||||
+2
@@ -10,6 +10,8 @@ Canvas/WebGL probes can show whether registered image sources entered a core ren
|
||||
|
||||
`mode=visual` captures desktop/mobile screenshots, loading, layout and diagnostics without clicking gameplay controls. It does not prove that the game loop works. `mode=gameplay` uses the fixed approved scenario (`generic-v1` by default, or the existing `tetris-v1` / `lane-defense-v1` contract), not an agent-written autopilot. It exercises the scenario independently in desktop and mobile viewports and requires both results. The receipt declares `gameplayViewports` separately from `visualViewports`. Older reports with only a desktop `playtest` result do not prove mobile gameplay. These bounded scenarios do not establish completion of an arbitrary long level.
|
||||
|
||||
Use each viewport's `gameplayResults` to diagnose failures: start with its first `diagnostics` entry, then inspect `assertions` and the recorded initial/final phase, sequence and level. An assertion with `passed=false` may not have run because an earlier step blocked; do not treat every unmet assertion as an independent defect. Missing viewport evidence is not a pass. In visual mode, `gameplayResults` is empty because gameplay was not run. `reportPath` is a private host evidence reference; use the returned diagnostics instead of trying to read that report.
|
||||
|
||||
For `generic-v1`, expose exactly one `script#playable-web-game-state[type="application/json"]` whose JSON is a projection of actual game state:
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": "agc-skill-pack.v1",
|
||||
"version": "2026-08-26.35",
|
||||
"version": "2026-08-26.37",
|
||||
"skills": [
|
||||
{
|
||||
"name": "agc-unity-editor",
|
||||
@@ -140,7 +140,7 @@
|
||||
"references/browser-evidence-contract.md",
|
||||
"references/runner-physics.mjs"
|
||||
],
|
||||
"sha256": "e0e7bdca529e068ce2974a5e312a267108ab5acb20e8f9d7c0c22d049d90b796"
|
||||
"sha256": "6800059c4e7bae70b0b42ec47175fc85e38d789e1759dca5df2067f715dc0657"
|
||||
},
|
||||
{
|
||||
"name": "agc-client-projection",
|
||||
|
||||
@@ -30,6 +30,8 @@ mod direct_runtime;
|
||||
mod direct_tool_bridge;
|
||||
mod direct_tools_mcp;
|
||||
mod direct_validation;
|
||||
#[cfg(test)]
|
||||
pub(crate) use direct_validation::browser_tool_result;
|
||||
mod generation;
|
||||
pub(crate) mod json_sidecar;
|
||||
pub(crate) mod redaction;
|
||||
|
||||
@@ -94,11 +94,14 @@ impl Drop for ActiveClaudeCodeTurnGuard {
|
||||
}
|
||||
}
|
||||
|
||||
struct ExternalMcpStopGuard;
|
||||
struct ExternalMcpStopGuard {
|
||||
root: PathBuf,
|
||||
token: String,
|
||||
}
|
||||
|
||||
impl Drop for ExternalMcpStopGuard {
|
||||
fn drop(&mut self) {
|
||||
stop_external_mcp_loopback();
|
||||
stop_external_mcp_loopback_for_root(&self.root, &self.token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -960,9 +963,13 @@ pub(crate) async fn direct_game_creator_claude_code_chat_at(
|
||||
observer: Option<&mut (dyn FnMut(TurnObservation) + Send)>,
|
||||
) -> Result<String, String> {
|
||||
direct_turn_trace("claude-executor-enter");
|
||||
let (mcp_url, mcp_token) = start_external_mcp_loopback(root, llm.web_search_enabled).await?;
|
||||
let (mcp_url, mcp_token) =
|
||||
start_external_mcp_loopback_for_direct_turn(root, llm.web_search_enabled).await?;
|
||||
direct_turn_trace("claude-mcp-loopback-ready");
|
||||
let _mcp_guard = ExternalMcpStopGuard;
|
||||
let _mcp_guard = ExternalMcpStopGuard {
|
||||
root: root.to_path_buf(),
|
||||
token: mcp_token.clone(),
|
||||
};
|
||||
let home = crate::project::resolve_local_project_path(root, ".agent/runtime/claude-code/home")?;
|
||||
std::fs::create_dir_all(&home)
|
||||
.map_err(|error| format!("创建 cc Direct 隔离目录失败:{error}"))?;
|
||||
|
||||
@@ -4236,8 +4236,9 @@ pub(crate) async fn start_direct_tool_bridge(
|
||||
pub(crate) async fn start_external_client_tool_bridge(
|
||||
root: &Path,
|
||||
controlled_web_search: bool,
|
||||
direct_turn_execution: bool,
|
||||
) -> Result<DirectToolBridge, String> {
|
||||
start_tool_bridge_for_source(root, controlled_web_search, false).await
|
||||
start_tool_bridge_for_source(root, controlled_web_search, direct_turn_execution).await
|
||||
}
|
||||
|
||||
async fn start_tool_bridge_for_source(
|
||||
@@ -4421,6 +4422,115 @@ pub(super) fn drain_analytics_test_writer(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn browser_gameplay_results_reach_model_with_matching_viewports_and_screenshots() {
|
||||
for passed in [false, true] {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let evidence = if passed {
|
||||
super::super::direct_validation::tests::browser_evidence_fixture(temp.path())
|
||||
} else {
|
||||
super::super::direct_validation::tests::failed_browser_evidence_fixture(temp.path())
|
||||
};
|
||||
for viewport in &evidence.viewport_results {
|
||||
image::RgbaImage::new(2, 2)
|
||||
.save(&viewport.screenshot_path)
|
||||
.unwrap();
|
||||
}
|
||||
let receipt = super::super::direct_validation::browser_tool_result(
|
||||
temp.path(),
|
||||
&evidence,
|
||||
Some(crate::browser::BrowserPlaytestScenario::GenericV1),
|
||||
1,
|
||||
);
|
||||
assert!(evidence.viewport_results.iter().all(|result| result.passed));
|
||||
assert_eq!(receipt["passed"], passed);
|
||||
let outcome = bridge_validation_result(
|
||||
temp.path(),
|
||||
Ok(receipt.clone()),
|
||||
|report, screenshots| BrowserPlaytestError::PlaytestNotPassed {
|
||||
report,
|
||||
screenshots,
|
||||
},
|
||||
)
|
||||
.map_err(|failure| {
|
||||
let error = ToolCallError::from(&failure);
|
||||
assert_eq!(error.error["PlaytestNotPassed"]["report"], receipt);
|
||||
assert!(error.error["PlaytestNotPassed"]
|
||||
.get("screenshots")
|
||||
.is_none());
|
||||
error
|
||||
});
|
||||
let state = direct_tool_bridge_state(temp.path().to_path_buf());
|
||||
let response =
|
||||
compose_direct_tool_outcome(&state, "agc_browser_playtest", "{}", false, outcome);
|
||||
assert_eq!(response["isError"], !passed);
|
||||
let text = response["content"][0]["text"].as_str().unwrap();
|
||||
let model_receipt: Value =
|
||||
serde_json::from_str(&text[text.find('{').unwrap()..]).unwrap();
|
||||
assert_eq!(model_receipt, receipt);
|
||||
assert_eq!(
|
||||
response["content"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|block| block["type"] == "image")
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
let summary = model_receipt["summary"].as_str().unwrap();
|
||||
for (index, entry) in evidence.viewport_playtests.iter().enumerate() {
|
||||
let report = &model_receipt["gameplayResults"][index];
|
||||
assert_eq!(
|
||||
report["viewport"],
|
||||
serde_json::to_value(entry.viewport).unwrap()
|
||||
);
|
||||
assert_eq!(report["passed"], entry.result.passed);
|
||||
assert_eq!(
|
||||
report["assertions"],
|
||||
serde_json::to_value(&entry.result.assertions).unwrap()
|
||||
);
|
||||
assert_eq!(report["finalPhase"], "ready");
|
||||
assert_eq!(
|
||||
report["finalSequence"],
|
||||
entry.result.final_sequence.unwrap()
|
||||
);
|
||||
let viewport = report["viewport"].as_str().unwrap();
|
||||
let line = summary
|
||||
.lines()
|
||||
.find(|line| line.starts_with(&format!("{viewport}: gameplayPassed=")))
|
||||
.unwrap();
|
||||
assert!(line.starts_with(&format!(
|
||||
"{viewport}: gameplayPassed={}",
|
||||
entry.result.passed
|
||||
)));
|
||||
if let Some(reason) = entry.result.diagnostics.first() {
|
||||
assert_eq!(report["diagnostics"][0], reason.as_str());
|
||||
assert!(line.contains(reason));
|
||||
}
|
||||
if let Some(assertion) = entry
|
||||
.result
|
||||
.assertions
|
||||
.iter()
|
||||
.find(|assertion| !assertion.passed)
|
||||
{
|
||||
assert!(line.contains(&assertion.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn external_client_bridge_can_be_enabled_for_a_direct_turn() {
|
||||
let temporary = tempfile::tempdir().unwrap();
|
||||
init_local_game_project_at(temporary.path(), "external-direct-turn", "外部直连回合")
|
||||
.unwrap();
|
||||
let bridge = start_external_client_tool_bridge(temporary.path(), false, true)
|
||||
.await
|
||||
.expect("direct Claude bridge");
|
||||
assert!(bridge.state.direct_turn_execution);
|
||||
drop(bridge);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_cancellation_releases_queued_image_without_a_new_post() {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
@@ -35,12 +35,16 @@ const EXTERNAL_MCP_RESPONSE_SUMMARY_MAX_CHARS: usize = 240;
|
||||
const EXTERNAL_MCP_JOURNAL_MAX_BYTES: u64 = 8 * 1024 * 1024;
|
||||
const EXTERNAL_MCP_JOURNAL_RELATIVE_PATH: &str = ".agent/conversations/codex-responses.jsonl";
|
||||
static EXTERNAL_MCP_JOURNAL_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
static EXTERNAL_MCP_SERVER: OnceLock<Mutex<Option<ExternalMcpServer>>> = OnceLock::new();
|
||||
static EXTERNAL_MCP_SERVER: OnceLock<
|
||||
Mutex<std::collections::BTreeMap<PathBuf, ExternalMcpServer>>,
|
||||
> = OnceLock::new();
|
||||
tokio::task_local! {
|
||||
static EXTERNAL_MCP_BRIDGE_URL: String;
|
||||
}
|
||||
|
||||
pub(crate) struct ExternalMcpServer {
|
||||
token: String,
|
||||
_direct_turn_guard: Option<super::direct_tool_bridge::DirectToolBridgeTurnGuard>,
|
||||
_bridge: super::direct_tool_bridge::DirectToolBridge,
|
||||
task: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
@@ -1759,6 +1763,21 @@ async fn handle_external_mcp_http_request(
|
||||
pub(crate) async fn start_external_mcp_loopback(
|
||||
root: &Path,
|
||||
controlled_web_search: bool,
|
||||
) -> Result<(String, String), String> {
|
||||
start_external_mcp_loopback_with_mode(root, controlled_web_search, false).await
|
||||
}
|
||||
|
||||
pub(crate) async fn start_external_mcp_loopback_for_direct_turn(
|
||||
root: &Path,
|
||||
controlled_web_search: bool,
|
||||
) -> Result<(String, String), String> {
|
||||
start_external_mcp_loopback_with_mode(root, controlled_web_search, true).await
|
||||
}
|
||||
|
||||
async fn start_external_mcp_loopback_with_mode(
|
||||
root: &Path,
|
||||
controlled_web_search: bool,
|
||||
direct_turn_execution: bool,
|
||||
) -> Result<(String, String), String> {
|
||||
let root = validate_direct_tools_project_root(root)?;
|
||||
let session = current_platform_session()
|
||||
@@ -1771,12 +1790,20 @@ pub(crate) async fn start_external_mcp_loopback(
|
||||
let address = listener
|
||||
.local_addr()
|
||||
.map_err(|error| format!("读取客户端 MCP 地址失败:{error}"))?;
|
||||
let bridge =
|
||||
super::direct_tool_bridge::start_external_client_tool_bridge(&root, controlled_web_search)
|
||||
.await?;
|
||||
let bridge = super::direct_tool_bridge::start_external_client_tool_bridge(
|
||||
&root,
|
||||
controlled_web_search,
|
||||
direct_turn_execution,
|
||||
)
|
||||
.await?;
|
||||
let direct_turn_guard = if direct_turn_execution {
|
||||
Some(bridge.begin_user_turn()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let state = ExternalMcpHttpState {
|
||||
bridge_url: bridge.url().to_string(),
|
||||
root,
|
||||
root: root.clone(),
|
||||
token: token.clone(),
|
||||
session_user_id: session.user_id,
|
||||
session_identity_generation: session.identity_generation,
|
||||
@@ -1790,24 +1817,40 @@ pub(crate) async fn start_external_mcp_loopback(
|
||||
let _ = axum::serve(listener, app).await;
|
||||
});
|
||||
let url = format!("http://127.0.0.1:{}{route}", address.port());
|
||||
let registry = EXTERNAL_MCP_SERVER.get_or_init(|| Mutex::new(None));
|
||||
let registry =
|
||||
EXTERNAL_MCP_SERVER.get_or_init(|| Mutex::new(std::collections::BTreeMap::new()));
|
||||
let mut guard = registry
|
||||
.lock()
|
||||
.map_err(|_| "客户端 MCP 服务注册表不可用".to_string())?;
|
||||
if let Some(previous) = guard.take() {
|
||||
drop(previous);
|
||||
}
|
||||
*guard = Some(ExternalMcpServer {
|
||||
_bridge: bridge,
|
||||
task,
|
||||
});
|
||||
guard.insert(
|
||||
root,
|
||||
ExternalMcpServer {
|
||||
token: token.clone(),
|
||||
_direct_turn_guard: direct_turn_guard,
|
||||
_bridge: bridge,
|
||||
task,
|
||||
},
|
||||
);
|
||||
Ok((url, token))
|
||||
}
|
||||
|
||||
pub(crate) fn stop_external_mcp_loopback() {
|
||||
if let Some(registry) = EXTERNAL_MCP_SERVER.get() {
|
||||
if let Ok(mut guard) = registry.lock() {
|
||||
guard.take();
|
||||
guard.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stop_external_mcp_loopback_for_root(root: &Path, token: &str) {
|
||||
let Ok(root) = root.canonicalize() else {
|
||||
return;
|
||||
};
|
||||
if let Some(registry) = EXTERNAL_MCP_SERVER.get() {
|
||||
if let Ok(mut guard) = registry.lock() {
|
||||
if guard.get(&root).is_some_and(|server| server.token == token) {
|
||||
guard.remove(&root);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2180,10 +2223,13 @@ mod tests {
|
||||
.unwrap();
|
||||
std::fs::create_dir(project.path().join(".agent")).unwrap();
|
||||
std::fs::write(project.path().join(".agent/manifest.json"), "{}").unwrap();
|
||||
let bridge =
|
||||
super::super::direct_tool_bridge::start_direct_tool_bridge(project.path(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
let bridge = super::super::direct_tool_bridge::start_external_client_tool_bridge(
|
||||
project.path(),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
for enabled in [false, true, false, true] {
|
||||
crate::builtin_plugins::set_enabled(
|
||||
crate::builtin_plugins::AGC_GODOT_EDITOR_PLUGIN_ID,
|
||||
@@ -2281,10 +2327,13 @@ mod tests {
|
||||
let project = crate::tests::canonical_test_tempdir("builtin-editor-mcp-");
|
||||
std::fs::create_dir_all(project.path().join(".agent")).unwrap();
|
||||
std::fs::write(project.path().join(".agent/manifest.json"), "{}").unwrap();
|
||||
let bridge =
|
||||
super::super::direct_tool_bridge::start_direct_tool_bridge(project.path(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
let bridge = super::super::direct_tool_bridge::start_external_client_tool_bridge(
|
||||
project.path(),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
for (cocos_enabled, unity_enabled) in
|
||||
[(false, false), (true, false), (false, true), (true, true)]
|
||||
{
|
||||
@@ -2419,10 +2468,13 @@ mod tests {
|
||||
let project = crate::tests::canonical_test_tempdir("builtin-mcp-project-");
|
||||
std::fs::create_dir_all(project.path().join(".agent")).unwrap();
|
||||
std::fs::write(project.path().join(".agent/manifest.json"), "{}").unwrap();
|
||||
let bridge =
|
||||
super::super::direct_tool_bridge::start_direct_tool_bridge(project.path(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
let bridge = super::super::direct_tool_bridge::start_external_client_tool_bridge(
|
||||
project.path(),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
for enabled in [false, true, false, true] {
|
||||
crate::builtin_plugins::set_enabled(
|
||||
crate::builtin_plugins::AGC_COCOS_EDITOR_PLUGIN_ID,
|
||||
|
||||
@@ -395,6 +395,137 @@ async fn finish_async(
|
||||
.map_err(|_| "验证回执任务退出".to_string())?
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GameplayViewportEvidence {
|
||||
viewport: crate::browser::BrowserValidationViewport,
|
||||
passed: bool,
|
||||
diagnostics: Vec<String>,
|
||||
assertions: Vec<crate::browser::BrowserPlaytestAssertion>,
|
||||
initial_phase: Option<crate::browser::BrowserPlaytestPhase>,
|
||||
initial_sequence: Option<u64>,
|
||||
initial_level: Option<u64>,
|
||||
final_phase: Option<crate::browser::BrowserPlaytestPhase>,
|
||||
final_sequence: Option<u64>,
|
||||
final_level: Option<u64>,
|
||||
}
|
||||
|
||||
/// 只投影模型修复所需的玩法事实;完整报告继续留在宿主私有目录。
|
||||
pub(crate) fn browser_tool_result(
|
||||
root: &Path,
|
||||
evidence: &crate::browser::BrowserValidationResult,
|
||||
scenario: Option<crate::browser::BrowserPlaytestScenario>,
|
||||
sequence: usize,
|
||||
) -> Value {
|
||||
use crate::browser::BrowserValidationViewport::{Desktop, Mobile};
|
||||
|
||||
let gameplay_results = if scenario.is_some() {
|
||||
[Desktop, Mobile]
|
||||
.into_iter()
|
||||
.map(|viewport| {
|
||||
let result = evidence
|
||||
.viewport_playtests
|
||||
.iter()
|
||||
.find(|entry| entry.viewport == viewport)
|
||||
.map(|entry| &entry.result);
|
||||
GameplayViewportEvidence {
|
||||
viewport,
|
||||
passed: result.is_some_and(|result| result.passed),
|
||||
diagnostics: result
|
||||
.map(|result| {
|
||||
result
|
||||
.diagnostics
|
||||
.iter()
|
||||
.take(4)
|
||||
.map(|message| truncate_agent_runtime_text(message, 511))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
vec![prompt_text!("direct.browser.gameplayMissing").into()]
|
||||
}),
|
||||
assertions: result
|
||||
.map(|result| result.assertions.clone())
|
||||
.unwrap_or_default(),
|
||||
initial_phase: result.and_then(|result| result.initial_phase),
|
||||
initial_sequence: result.and_then(|result| result.initial_sequence),
|
||||
initial_level: result.and_then(|result| result.initial_level),
|
||||
final_phase: result.and_then(|result| result.final_phase),
|
||||
final_sequence: result.and_then(|result| result.final_sequence),
|
||||
final_level: result.and_then(|result| result.final_level),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let gameplay_summary = if scenario.is_none() {
|
||||
prompt_text!("direct.browser.gameplayNotRun").to_string()
|
||||
} else {
|
||||
let lines = gameplay_results
|
||||
.iter()
|
||||
.map(|result| {
|
||||
format!(
|
||||
prompt_text!("direct.browser.gameplayViewport"),
|
||||
viewport = match result.viewport {
|
||||
Desktop => "desktop",
|
||||
Mobile => "mobile",
|
||||
},
|
||||
passed = result.passed,
|
||||
diagnostic = result
|
||||
.diagnostics
|
||||
.first()
|
||||
.map(String::as_str)
|
||||
.unwrap_or(prompt_text!("direct.browser.gameplayNoDiagnostic")),
|
||||
assertion = result
|
||||
.assertions
|
||||
.iter()
|
||||
.find(|assertion| !assertion.passed)
|
||||
.map(|assertion| assertion.name.as_str())
|
||||
.unwrap_or(prompt_text!("direct.browser.gameplayNoUnmetAssertion")),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
format!(
|
||||
prompt_text!("direct.browser.gameplayEvidence"),
|
||||
lines.join("\n")
|
||||
)
|
||||
};
|
||||
let summary = format!(
|
||||
"{}\n{}",
|
||||
render_direct_browser_tool_evidence(root, evidence, sequence),
|
||||
gameplay_summary
|
||||
);
|
||||
let screenshots: Vec<_> = evidence
|
||||
.viewport_results
|
||||
.iter()
|
||||
.filter_map(|viewport| {
|
||||
viewport
|
||||
.screenshot_path
|
||||
.strip_prefix(root)
|
||||
.ok()
|
||||
.map(|path| path.to_string_lossy().replace('\\', "/"))
|
||||
})
|
||||
.collect();
|
||||
let passed = evidence.passed
|
||||
&& scenario.is_none_or(|scenario| {
|
||||
crate::browser::required_viewport_playtests_passed(
|
||||
scenario,
|
||||
&evidence.viewport_playtests,
|
||||
)
|
||||
});
|
||||
json!({
|
||||
"kind":"browser", "mode":if scenario.is_some() {"gameplay"} else {"visual"},
|
||||
"scenario":scenario, "passed":passed,
|
||||
"scenarioFingerprint":scenario.map(crate::browser::browser_playtest_scenario_fingerprint),
|
||||
"summary":summary, "screenshots":screenshots,
|
||||
"reportPath":evidence.evidence.report_path.strip_prefix(root).ok().map(|path|path.to_string_lossy().replace('\\', "/")),
|
||||
"visualViewports":["desktop","mobile"],
|
||||
"gameplayViewports":evidence.viewport_playtests.iter().map(|entry|entry.viewport).collect::<Vec<_>>(),
|
||||
"gameplayResults":gameplay_results,
|
||||
"coverage":if scenario.is_some() {"dual-viewport-fixed-scenario"} else {"visual-only"}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn run_browser(root: &Path, arguments: &Value) -> Result<Value, String> {
|
||||
run_browser_with_budget(root, arguments, true).await
|
||||
}
|
||||
@@ -466,33 +597,9 @@ async fn run_browser_with_budget(
|
||||
.await;
|
||||
let (result, passed) = match evidence {
|
||||
Ok(evidence) => {
|
||||
let screenshots: Vec<_> = evidence
|
||||
.viewport_results
|
||||
.iter()
|
||||
.filter_map(|v| {
|
||||
v.screenshot_path
|
||||
.strip_prefix(root)
|
||||
.ok()
|
||||
.map(|p| p.to_string_lossy().replace('\\', "/"))
|
||||
})
|
||||
.collect();
|
||||
let gameplay_passed = scenario.is_none_or(|scenario| {
|
||||
crate::browser::required_viewport_playtests_passed(
|
||||
scenario,
|
||||
&evidence.viewport_playtests,
|
||||
)
|
||||
});
|
||||
let passed = evidence.passed && gameplay_passed;
|
||||
let summary = render_direct_browser_tool_evidence(root, &evidence, sequence);
|
||||
(
|
||||
json!({"kind":"browser", "mode":input.mode, "scenario":scenario,
|
||||
"scenarioFingerprint":scenario.map(crate::browser::browser_playtest_scenario_fingerprint), "summary":summary, "screenshots":screenshots,
|
||||
"reportPath":evidence.evidence.report_path.strip_prefix(root).ok().map(|p|p.to_string_lossy().replace('\\', "/")),
|
||||
"visualViewports":["desktop","mobile"],
|
||||
"gameplayViewports":evidence.viewport_playtests.iter().map(|v|v.viewport).collect::<Vec<_>>(),
|
||||
"coverage":if scenario.is_some() {"dual-viewport-fixed-scenario"} else {"visual-only"}}),
|
||||
passed,
|
||||
)
|
||||
let result = browser_tool_result(root, &evidence, scenario, sequence);
|
||||
let passed = result["passed"] == true;
|
||||
(result, passed)
|
||||
}
|
||||
Err(error) => (
|
||||
json!({"mode":input.mode,"needsReconciliation":error.starts_with("browser-cleanup-unconfirmed:"),"error":truncate_agent_runtime_text(&error, 1800)}),
|
||||
@@ -599,9 +706,147 @@ pub(super) async fn run_command(root: &Path, arguments: &Value) -> Result<Value,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub(super) mod tests {
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn browser_evidence_fixture(root: &Path) -> crate::browser::BrowserValidationResult {
|
||||
let mut viewports = Vec::new();
|
||||
let mut playtests = Vec::new();
|
||||
for viewport in ["desktop", "mobile"] {
|
||||
viewports.push(json!({
|
||||
"viewport":viewport, "width":1280, "height":720,
|
||||
"finalUrl":"http://127.0.0.1/", "title":"fixture", "readyState":"complete",
|
||||
"visibleTextSummary":"", "visibleTextCharacterCount":0, "domCharacterCount":0,
|
||||
"expectedText":[], "consoleErrors":[], "consoleWarnings":[], "exceptions":[],
|
||||
"failedRequests":[], "canvases":[], "diagnostics":[], "passed":true,
|
||||
"blockedPopupCount":0, "blockedDialogCount":0, "blockedDownloadCount":0,
|
||||
"blockedPermissionCount":0, "blockedServiceWorkerCount":0,
|
||||
"screenshotPath":root.join(format!("{viewport}.png"))
|
||||
}));
|
||||
playtests.push(json!({"viewport":viewport, "result":{
|
||||
"scenario":"generic-v1", "scenarioFingerprint":"fixture", "passed":true,
|
||||
"initialSequence":0, "initialPhase":"ready", "initialLevel":1,
|
||||
"finalSequence":3, "finalPhase":"ready", "finalLevel":1,
|
||||
"assertions":[
|
||||
{"name":"start-control-clicked", "passed":true},
|
||||
{"name":"start-phase-playing", "passed":true},
|
||||
{"name":"restart-control-clicked", "passed":true}
|
||||
], "diagnostics":[]
|
||||
}}));
|
||||
}
|
||||
serde_json::from_value(json!({
|
||||
"schemaVersion":"browser-validation.v1", "url":"http://127.0.0.1/",
|
||||
"browser":{"kind":"chrome", "product":"fixture", "protocolVersion":"1.3"},
|
||||
"passed":true, "viewportResults":viewports, "viewportPlaytests":playtests,
|
||||
"diagnostics":[], "completedAtUnixMs":1,
|
||||
"evidence":{"root":root, "reportPath":root.join("validation.json")}
|
||||
}))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn failed_browser_evidence_fixture(
|
||||
root: &Path,
|
||||
) -> crate::browser::BrowserValidationResult {
|
||||
let mut evidence = browser_evidence_fixture(root);
|
||||
evidence.passed = false;
|
||||
for (index, entry) in evidence.viewport_playtests.iter_mut().enumerate() {
|
||||
entry.result.passed = false;
|
||||
entry.result.final_phase = Some(crate::browser::BrowserPlaytestPhase::Ready);
|
||||
entry.result.final_sequence = Some(index as u64);
|
||||
entry.result.diagnostics = vec![if index == 0 {
|
||||
"固定试玩控件 start 处于 disabled 状态".into()
|
||||
} else {
|
||||
"generic-v1 start 后必须进入 playing".into()
|
||||
}];
|
||||
for (assertion_index, assertion) in entry.result.assertions.iter_mut().enumerate() {
|
||||
assertion.passed = index == 1 && assertion_index == 0;
|
||||
}
|
||||
}
|
||||
evidence
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_tool_projection_distinguishes_gameplay_success_visual_only_and_missing_viewport() {
|
||||
use crate::browser::BrowserPlaytestScenario::GenericV1;
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
for mode in ["gameplay", "visual", "missing-mobile"] {
|
||||
let mut evidence = browser_evidence_fixture(root.path());
|
||||
let scenario = if mode == "visual" {
|
||||
evidence.viewport_playtests.clear();
|
||||
None
|
||||
} else {
|
||||
Some(GenericV1)
|
||||
};
|
||||
if mode == "missing-mobile" {
|
||||
evidence.viewport_playtests.pop();
|
||||
evidence.passed = false;
|
||||
}
|
||||
let receipt = browser_tool_result(root.path(), &evidence, scenario, 1);
|
||||
let summary = receipt["summary"].as_str().unwrap();
|
||||
assert_eq!(receipt["passed"], mode != "missing-mobile");
|
||||
if mode == "visual" {
|
||||
assert_eq!(receipt["gameplayResults"], json!([]));
|
||||
assert!(summary
|
||||
.lines()
|
||||
.any(|line| line.starts_with("玩法检查:未执行")));
|
||||
assert_eq!(receipt["coverage"], "visual-only");
|
||||
continue;
|
||||
}
|
||||
for (index, viewport) in ["desktop", "mobile"].iter().enumerate() {
|
||||
let result = &receipt["gameplayResults"][index];
|
||||
assert_eq!(result["viewport"], *viewport);
|
||||
if mode == "missing-mobile" && index == 1 {
|
||||
assert_eq!(result["passed"], false);
|
||||
assert!(result["diagnostics"][0].as_str().unwrap().contains("缺失"));
|
||||
assert!(summary.contains("mobile: gameplayPassed=false"));
|
||||
assert_eq!(result["assertions"], json!([]));
|
||||
assert_eq!(result["finalPhase"], Value::Null);
|
||||
} else {
|
||||
assert_eq!(result["passed"], true);
|
||||
assert_eq!(result["diagnostics"], json!([]));
|
||||
assert!(result["assertions"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|a| a["passed"] == true));
|
||||
assert!(summary.contains(&format!("{viewport}: gameplayPassed=true")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_tool_projection_bounds_diagnostics_without_losing_either_blocker() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mut evidence = failed_browser_evidence_fixture(root.path());
|
||||
for viewport in &mut evidence.viewport_playtests {
|
||||
viewport
|
||||
.result
|
||||
.diagnostics
|
||||
.extend((0..8).map(|_| "额外诊断".repeat(200)));
|
||||
}
|
||||
let receipt = browser_tool_result(
|
||||
root.path(),
|
||||
&evidence,
|
||||
Some(crate::browser::BrowserPlaytestScenario::GenericV1),
|
||||
1,
|
||||
);
|
||||
for (index, entry) in evidence.viewport_playtests.iter().enumerate() {
|
||||
let diagnostics = receipt["gameplayResults"][index]["diagnostics"]
|
||||
.as_array()
|
||||
.unwrap();
|
||||
assert_eq!(diagnostics.len(), 4);
|
||||
assert_eq!(diagnostics[0], entry.result.diagnostics[0]);
|
||||
assert!(diagnostics
|
||||
.iter()
|
||||
.all(|d| d.as_str().unwrap().chars().count() <= 512));
|
||||
assert!(receipt["summary"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains(&entry.result.diagnostics[0]));
|
||||
}
|
||||
}
|
||||
|
||||
fn project() -> (tempfile::TempDir, std::path::PathBuf) {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = temp.path().join("project");
|
||||
|
||||
@@ -1514,6 +1514,41 @@ async fn real_chrome_generic_playtest_pointer_events_keeps_unavailable_controls_
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires an installed Chrome/Chromium/Edge and explicit local browser execution"]
|
||||
async fn real_chrome_generic_playtest_reports_start_click_without_playing_phase() {
|
||||
let result = run_stable_generic_viewport_fixture(false, Some(
|
||||
r#"<button type="button" data-playtest-id="start" data-start-phase="ready">Start</button>"#,
|
||||
)).await;
|
||||
assert!(!result.passed);
|
||||
assert!(result
|
||||
.viewport_results
|
||||
.iter()
|
||||
.all(|viewport| viewport.passed));
|
||||
for entry in &result.viewport_playtests {
|
||||
let playtest = &entry.result;
|
||||
assert!(!playtest.passed);
|
||||
assert_eq!(playtest.final_phase, Some(BrowserPlaytestPhase::Ready));
|
||||
assert_eq!(playtest.final_sequence, Some(1));
|
||||
for (name, expected) in [
|
||||
("start-control-clicked", true),
|
||||
("start-phase-playing", false),
|
||||
] {
|
||||
assert_eq!(
|
||||
playtest
|
||||
.assertions
|
||||
.iter()
|
||||
.find(|assertion| assertion.name == name)
|
||||
.unwrap()
|
||||
.passed,
|
||||
expected
|
||||
);
|
||||
}
|
||||
let reason = &playtest.diagnostics[0];
|
||||
assert!(reason.contains("必须进入 playing"), "{reason}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_stable_generic_viewport_fixture(
|
||||
mobile_action_broken: bool,
|
||||
start_markup: Option<&str>,
|
||||
@@ -1556,7 +1591,7 @@ async fn run_stable_generic_viewport_fixture(
|
||||
};
|
||||
document.querySelector('[data-playtest-id="start"]').addEventListener('click', (event) => {
|
||||
if (event.isTrusted && state.phase === 'ready') {
|
||||
advance(() => { state.phase = 'playing'; });
|
||||
advance(() => { state.phase = event.currentTarget.dataset.startPhase || 'playing'; });
|
||||
}
|
||||
});
|
||||
const primary = document.querySelector('[data-playtest-id="primary-action"]');
|
||||
@@ -1641,6 +1676,57 @@ async fn run_stable_generic_viewport_fixture(
|
||||
let persisted: serde_json::Value =
|
||||
serde_json::from_slice(&fs::read(&result.evidence.report_path).unwrap()).unwrap();
|
||||
assert_eq!(persisted["viewportPlaytests"].as_array().unwrap().len(), 2);
|
||||
let receipt = crate::agent::browser_tool_result(
|
||||
evidence.path(),
|
||||
&result,
|
||||
Some(BrowserPlaytestScenario::GenericV1),
|
||||
1,
|
||||
);
|
||||
assert_eq!(receipt["passed"], persisted["passed"]);
|
||||
for (index, entry) in persisted["viewportPlaytests"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let projected = &receipt["gameplayResults"][index];
|
||||
assert_eq!(projected["viewport"], entry["viewport"]);
|
||||
let viewport = entry["viewport"].as_str().unwrap();
|
||||
let line = receipt["summary"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.lines()
|
||||
.find(|line| line.starts_with(&format!("{viewport}: gameplayPassed=")))
|
||||
.unwrap();
|
||||
assert!(line.starts_with(&format!(
|
||||
"{viewport}: gameplayPassed={}",
|
||||
entry["result"]["passed"]
|
||||
)));
|
||||
for field in [
|
||||
"passed",
|
||||
"assertions",
|
||||
"initialPhase",
|
||||
"initialSequence",
|
||||
"initialLevel",
|
||||
"finalPhase",
|
||||
"finalSequence",
|
||||
"finalLevel",
|
||||
] {
|
||||
assert_eq!(projected[field], entry["result"][field], "{field}");
|
||||
}
|
||||
if let Some(reason) = entry["result"]["diagnostics"][0].as_str() {
|
||||
assert_eq!(projected["diagnostics"][0], reason);
|
||||
assert!(line.contains(reason));
|
||||
}
|
||||
if let Some(assertion) = entry["result"]["assertions"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|assertion| assertion["passed"] == false)
|
||||
{
|
||||
assert!(line.contains(assertion["name"].as_str().unwrap()));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
+18
-3
@@ -96,9 +96,7 @@ function directModelCallText(kind: ModelCallKind | null | undefined): string {
|
||||
case 'upstreamFailed':
|
||||
return (
|
||||
(kind.native ? directNativeKindText(kind.native) : null) ??
|
||||
(kind.statusCode >= 500
|
||||
? '智能服务暂时不可用,请稍后重试'
|
||||
: '智能创作请求无效,请稍后重试')
|
||||
directUpstreamStatusText(kind.statusCode)
|
||||
);
|
||||
case 'paidCreditsInsufficient':
|
||||
return '泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。';
|
||||
@@ -113,6 +111,23 @@ function directModelCallText(kind: ModelCallKind | null | undefined): string {
|
||||
}
|
||||
}
|
||||
|
||||
function directUpstreamStatusText(statusCode: number): string {
|
||||
switch (statusCode) {
|
||||
case 401:
|
||||
return '上游服务拒绝认证(HTTP 401),请重新登录后重试';
|
||||
case 403:
|
||||
return '上游服务拒绝访问(HTTP 403),请检查账号权限后重试';
|
||||
case 408:
|
||||
return '上游请求超时(HTTP 408),请稍后重试';
|
||||
case 429:
|
||||
return '上游服务繁忙(HTTP 429),请稍后重试';
|
||||
default:
|
||||
return statusCode >= 500
|
||||
? `智能服务暂时不可用(HTTP ${statusCode}),请稍后重试`
|
||||
: `智能创作请求被上游拒绝(HTTP ${statusCode}),请检查请求后重试`;
|
||||
}
|
||||
}
|
||||
|
||||
function directTurnDeadlineText(deadline: Deadline): string {
|
||||
switch (deadline) {
|
||||
case 'response-idle':
|
||||
|
||||
@@ -11,8 +11,11 @@ import {
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ProjectResourceCanvasPosition } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import type { ProjectResourceCanvasCategory } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import {
|
||||
isProjectResourceCanvasCategory,
|
||||
type ProjectResourceCanvasCategory,
|
||||
type ProjectResourceCanvasPosition,
|
||||
} from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import {
|
||||
RESOURCE_CANVAS_CARD_WIDTH,
|
||||
RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP,
|
||||
@@ -130,7 +133,11 @@ function OverlayHarness({
|
||||
overlayRef?: React.Ref<ResourceDependencyOverlayHandle>;
|
||||
}) {
|
||||
const sections = Array.from(
|
||||
new Set(positions.map((position) => position.section)),
|
||||
new Set(
|
||||
positions
|
||||
.map((position) => position.section)
|
||||
.filter(isProjectResourceCanvasCategory),
|
||||
),
|
||||
);
|
||||
return React.createElement(
|
||||
'div',
|
||||
@@ -809,7 +816,7 @@ describe('ResourceDependencyOverlay', () => {
|
||||
it('keeps drag previews in plane-local coordinates', async () => {
|
||||
const getBoundingClientRect = vi
|
||||
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
|
||||
.mockImplementation(function () {
|
||||
.mockImplementation(function (this: HTMLElement) {
|
||||
const isPlane = this.hasAttribute('data-resource-section-plane');
|
||||
return {
|
||||
x: isPlane ? 160 : 20,
|
||||
@@ -1046,7 +1053,9 @@ describe('ResourceDependencyOverlay', () => {
|
||||
.map((value) =>
|
||||
value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4,
|
||||
);
|
||||
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
||||
return (
|
||||
channels[0]! * 0.2126 + channels[1]! * 0.7152 + channels[2]! * 0.0722
|
||||
);
|
||||
};
|
||||
const lineLuminance = luminance('c45f20');
|
||||
const canvasLuminance = luminance('fffdfa');
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ResourceEditSurface } from '../src/view/project-development/ResourceEditSurface';
|
||||
import {
|
||||
type ResourceEditSubmitInput,
|
||||
ResourceEditSurface,
|
||||
} from '../src/view/project-development/ResourceEditSurface';
|
||||
import { fireEvent, render, screen, waitFor } from './appSurface/harness';
|
||||
|
||||
describe('统一资源编辑面板', () => {
|
||||
it('失败后锁定原请求字段并使用相同输入重试', async () => {
|
||||
const submit = vi
|
||||
.fn<(input: { prompt: string; assetName: string }) => Promise<void>>()
|
||||
.fn<[input: ResourceEditSubmitInput], Promise<void>>()
|
||||
.mockRejectedValueOnce(new Error('网络暂不可用'))
|
||||
.mockResolvedValueOnce(undefined);
|
||||
render(
|
||||
|
||||
@@ -14,9 +14,9 @@ const platformSession = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
// 与仓库其它 App 内调用方同口径:通过 `window.__TAURI__.core.invoke` 解析原生桥。
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke: (...args: unknown[]) => invoke(...args) },
|
||||
} as never;
|
||||
});
|
||||
vi.mock('../src/services/platformSession', () => ({
|
||||
currentPlatformSessionGeneration: () => platformSession.generation,
|
||||
requestPlatformSessionRefresh: (...args: unknown[]) =>
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
getClientProfileWalletLedger,
|
||||
redeemClientProfileRewardCode,
|
||||
} from '../src/services/accountHost';
|
||||
import { installTauriRuntime } from './tauriRuntimeFake';
|
||||
|
||||
beforeEach(() => {
|
||||
invoke.mockReset();
|
||||
|
||||
@@ -10,76 +10,51 @@ import { installUnhandledRejectionBridge } from '../unhandledRejectionBridge';
|
||||
import {
|
||||
act,
|
||||
AuthenticatedClient,
|
||||
createTauriEventBridge,
|
||||
expect,
|
||||
fireEvent,
|
||||
installTauriRuntime,
|
||||
it,
|
||||
React,
|
||||
render,
|
||||
resetTauriRuntime,
|
||||
screen,
|
||||
testAuthUser,
|
||||
vi,
|
||||
waitFor,
|
||||
} from './harness';
|
||||
|
||||
type AuthEventBridge = {
|
||||
listeners: Map<string, (event: { payload: unknown }) => void>;
|
||||
emit: (event: string, payload: unknown) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 注入 Tauri 事件替身。
|
||||
*
|
||||
* jsdom 没有 `__TAURI_INTERNALS__`,`subscribeTauriEvent` 会退回
|
||||
* `window.__TAURI__.event.listen`,所以这里按同一入口登记回调。
|
||||
* `window.__TAURI__.event.listen`,所以按同一入口登记回调:安装走
|
||||
* harness.installTauriRuntime,事件注册 / 派发由 harness.createTauriEventBridge 负责。
|
||||
*/
|
||||
function installTauriBridge(
|
||||
invoke: (command: string, args?: Record<string, unknown>) => Promise<unknown>,
|
||||
options: { withEvents?: boolean } = {},
|
||||
): AuthEventBridge {
|
||||
const listeners = new Map<string, (event: { payload: unknown }) => void>();
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
...(options.withEvents
|
||||
? {
|
||||
event: {
|
||||
listen: async (
|
||||
event: string,
|
||||
handler: (payload: { payload: unknown }) => void,
|
||||
) => {
|
||||
listeners.set(event, handler);
|
||||
return () => listeners.delete(event);
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
} as never;
|
||||
return {
|
||||
listeners,
|
||||
emit: (event, payload) => {
|
||||
const handler = listeners.get(event);
|
||||
if (handler) handler({ payload });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function renderAuthSurface(
|
||||
invoke: (command: string, args?: Record<string, unknown>) => Promise<unknown>,
|
||||
options: { withEvents?: boolean } = {},
|
||||
) {
|
||||
const bridge = installTauriBridge(invoke, options);
|
||||
const bridge = createTauriEventBridge();
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
...(options.withEvents ? { event: { listen: bridge.listen } } : {}),
|
||||
});
|
||||
render(
|
||||
React.createElement(AuthenticatedClient, null, ({ user, logout }) =>
|
||||
React.createElement(
|
||||
'main',
|
||||
{ 'aria-label': '已登录' },
|
||||
React.createElement('span', null, user.displayName),
|
||||
// React 19 的 createElement 的 children 形参只接受 ReactNode,渲染函数子节点改传进 props。
|
||||
React.createElement(AuthenticatedClient, {
|
||||
children: ({ user, logout }) =>
|
||||
React.createElement(
|
||||
'button',
|
||||
{ type: 'button', onClick: logout },
|
||||
'退出',
|
||||
'main',
|
||||
{ 'aria-label': '已登录' },
|
||||
React.createElement('span', null, user.displayName),
|
||||
React.createElement(
|
||||
'button',
|
||||
{ type: 'button', onClick: logout },
|
||||
'退出',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
);
|
||||
return bridge;
|
||||
}
|
||||
@@ -118,7 +93,7 @@ function captureWindowRejections() {
|
||||
export function registerAuthTests() {
|
||||
afterEach(() => {
|
||||
resetPlatformSessionForTests();
|
||||
delete window.__TAURI__;
|
||||
resetTauriRuntime();
|
||||
});
|
||||
|
||||
it('throws a startup timeout through to unhandledrejection and leaves the loading state', async () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
createProjectChatRuntimeHarness,
|
||||
expect,
|
||||
fireEvent,
|
||||
installTauriRuntime,
|
||||
it,
|
||||
pickProjectFromLauncher,
|
||||
React,
|
||||
@@ -108,10 +109,10 @@ async function openDirectCodexSurface(
|
||||
return chatHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
};
|
||||
});
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
pickProjectFromLauncher(DYNAMIC_GAME_PROJECT_PATH);
|
||||
const surface = await screen.findByLabelText('陶泥儿项目对话');
|
||||
@@ -325,7 +326,7 @@ export function registerChatComposerControlTests() {
|
||||
const manifestAssets = [
|
||||
{
|
||||
id: 'asset:hero',
|
||||
kind: 'character',
|
||||
kind: 'character' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/hero.png',
|
||||
source: { kind: 'uploaded' as const },
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import { dispatchResourceReferenceInsert } from '../../src/features/project-workspace/resourceReferences';
|
||||
import {
|
||||
act,
|
||||
@@ -5,6 +7,7 @@ import {
|
||||
createProjectChatRuntimeHarness,
|
||||
expect,
|
||||
fireEvent,
|
||||
installTauriRuntime,
|
||||
it,
|
||||
React,
|
||||
render,
|
||||
@@ -13,16 +16,43 @@ import {
|
||||
waitFor,
|
||||
} from './harness';
|
||||
|
||||
type ChatRuntimeHarness = ReturnType<typeof createProjectChatRuntimeHarness>;
|
||||
|
||||
/**
|
||||
* 把夹具的 invoke / listen 装进 `window.__TAURI__`:转换点只有 harness.installTauriRuntime 一处。
|
||||
*/
|
||||
function installChatRuntime(harness: ChatRuntimeHarness) {
|
||||
installTauriRuntime({
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* harness.invoke 的返回类型被 harness 内全部命令分支收敛成字面量联合,用例覆盖命令时要返回
|
||||
* 联合外的自定义值(例如导入文件路径),这里按固定签名 mock 取用,只放宽返回值类型。
|
||||
*/
|
||||
type LooseInvokeMock = Mock<
|
||||
[command: string, args?: Record<string, unknown>],
|
||||
Promise<unknown>
|
||||
>;
|
||||
|
||||
function looseInvoke(harness: ChatRuntimeHarness): LooseInvokeMock {
|
||||
return harness.invoke as unknown as LooseInvokeMock;
|
||||
}
|
||||
|
||||
// App 的 props 类型未导出,React 19 的 createElement 重载推断不出参数类型,这里显式转成组件类型。
|
||||
const AppComponent = App as React.ComponentType<
|
||||
NonNullable<React.ComponentProps<typeof App>>
|
||||
>;
|
||||
|
||||
function renderDesignAgent(
|
||||
harness: ReturnType<typeof createProjectChatRuntimeHarness>,
|
||||
) {
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
installChatRuntime(harness);
|
||||
window.history.pushState({}, '', '/');
|
||||
return render(
|
||||
React.createElement(App, {
|
||||
React.createElement(AppComponent, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
planningStartMode: true,
|
||||
}),
|
||||
@@ -128,13 +158,14 @@ export function registerDesignAgentSurfaceTests() {
|
||||
designAgentView: designConversationView(),
|
||||
designAgentContinueView: designConversationView(),
|
||||
});
|
||||
const originalInvoke = harness.invoke.getMockImplementation()!;
|
||||
const invoke = looseInvoke(harness);
|
||||
const originalInvoke = invoke.getMockImplementation()!;
|
||||
let failImport = true;
|
||||
let finishImport!: () => void;
|
||||
const importGate = new Promise<void>((resolve) => {
|
||||
finishImport = resolve;
|
||||
});
|
||||
harness.invoke.mockImplementation(async (command, args) => {
|
||||
invoke.mockImplementation(async (command, args) => {
|
||||
if (command === 'import_design_workspace_file') {
|
||||
await importGate;
|
||||
if (args?.fileName === '补充.md' && failImport)
|
||||
@@ -225,8 +256,9 @@ export function registerDesignAgentSurfaceTests() {
|
||||
const harness = createProjectChatRuntimeHarness({
|
||||
designAgentView: designApprovalView(),
|
||||
});
|
||||
const originalInvoke = harness.invoke.getMockImplementation()!;
|
||||
harness.invoke.mockImplementation(async (command, args) =>
|
||||
const invoke = looseInvoke(harness);
|
||||
const originalInvoke = invoke.getMockImplementation()!;
|
||||
invoke.mockImplementation(async (command, args) =>
|
||||
command === 'import_design_workspace_file'
|
||||
? 'references/需求.md'
|
||||
: originalInvoke(command, args),
|
||||
@@ -368,13 +400,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
],
|
||||
},
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
installChatRuntime(harness);
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(AppComponent, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
planningStartMode: true,
|
||||
}),
|
||||
@@ -412,9 +441,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
const harness = createProjectChatRuntimeHarness({
|
||||
designAgentView: designConversationView(),
|
||||
});
|
||||
const originalInvoke = harness.invoke.getMockImplementation()!;
|
||||
const invoke = looseInvoke(harness);
|
||||
const originalInvoke = invoke.getMockImplementation()!;
|
||||
let finish!: (view: unknown) => void;
|
||||
harness.invoke.mockImplementation((command, args) =>
|
||||
invoke.mockImplementation((command, args) =>
|
||||
command === 'continue_design_agent_session'
|
||||
? new Promise((resolve) => {
|
||||
finish = resolve;
|
||||
@@ -468,12 +498,13 @@ export function registerDesignAgentSurfaceTests() {
|
||||
const harness = createProjectChatRuntimeHarness({
|
||||
designAgentView: { ...designConversationView(), messages: [history] },
|
||||
});
|
||||
const originalInvoke = harness.invoke.getMockImplementation()!;
|
||||
const invoke = looseInvoke(harness);
|
||||
const originalInvoke = invoke.getMockImplementation()!;
|
||||
const requests: {
|
||||
clientTurnId: string;
|
||||
finish: (view: unknown) => void;
|
||||
}[] = [];
|
||||
harness.invoke.mockImplementation((command, args) =>
|
||||
invoke.mockImplementation((command, args) =>
|
||||
command === 'continue_design_agent_session'
|
||||
? new Promise((resolve) => {
|
||||
requests.push({
|
||||
@@ -490,7 +521,7 @@ export function registerDesignAgentSurfaceTests() {
|
||||
await setComposerText(input, '继续');
|
||||
fireEvent.submit(input.closest('form')!);
|
||||
await waitFor(() => expect(requests).toHaveLength(1));
|
||||
const request = requests[0];
|
||||
const request = requests[0]!;
|
||||
const messageId = `${request.clientTurnId}:response:0`;
|
||||
const reply = '整块返回也逐步显示';
|
||||
const terminal = {
|
||||
@@ -544,7 +575,7 @@ export function registerDesignAgentSurfaceTests() {
|
||||
screen.getByRole('button', { name: '思考中' }).hasAttribute('disabled'),
|
||||
).toBe(true);
|
||||
expect(screen.queryByText('迟到旧回复')).toBeNull();
|
||||
await act(async () => requests[1].finish(terminal));
|
||||
await act(async () => requests[1]!.finish(terminal));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -733,13 +764,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
const harness = createProjectChatRuntimeHarness({
|
||||
designAgentView: designApprovalView(),
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
installChatRuntime(harness);
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(AppComponent, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
planningStartMode: true,
|
||||
}),
|
||||
@@ -779,13 +807,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
},
|
||||
},
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
installChatRuntime(harness);
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(AppComponent, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
planningStartMode: true,
|
||||
}),
|
||||
@@ -817,13 +842,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
designAgentView: designConversationView(),
|
||||
designAgentContinueView: designConversationView(),
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
installChatRuntime(harness);
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(AppComponent, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
planningStartMode: true,
|
||||
}),
|
||||
@@ -887,13 +909,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
const harness = createProjectChatRuntimeHarness({
|
||||
designAgentView: designHistoryView(),
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
installChatRuntime(harness);
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(AppComponent, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
planningStartMode: true,
|
||||
}),
|
||||
@@ -912,10 +931,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
(element) => element.getAttribute('data-agent-content') === 'process',
|
||||
),
|
||||
).toBe(true);
|
||||
fireEvent.click(summaries[0]);
|
||||
expect(details[0].open).toBe(true);
|
||||
expect(details[1].open).toBe(false);
|
||||
await waitFor(() => expect(summaries[0].textContent).toBe('思考过程'));
|
||||
fireEvent.click(summaries[0]!);
|
||||
expect(details[0]!.open).toBe(true);
|
||||
expect(details[1]!.open).toBe(false);
|
||||
await waitFor(() => expect(summaries[0]!.textContent).toBe('思考过程'));
|
||||
});
|
||||
|
||||
it('画布派发的「引用」落进策划输入盒草稿', async () => {
|
||||
|
||||
@@ -15,6 +15,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { resetLlmModelCatalogCacheForTest } from '../../src/services/llmModelCatalog';
|
||||
import { useLauncherHomeDraftStore } from '../../src/view/home/useHomeDraftStore';
|
||||
import {
|
||||
createTauriEventBridge,
|
||||
installTauriRuntime,
|
||||
resetTauriRuntime,
|
||||
} from '../tauriRuntimeFake';
|
||||
|
||||
const nativeClipboardMock = vi.hoisted(() => ({
|
||||
text: '',
|
||||
@@ -195,20 +200,22 @@ function pickProjectFromLauncher(
|
||||
projectPath: string,
|
||||
action: '打开项目' | '新建项目' = '打开项目',
|
||||
) {
|
||||
const invoke = window.__TAURI__?.core?.invoke;
|
||||
if (!invoke) {
|
||||
const tauri = window.__TAURI__;
|
||||
const invoke = tauri?.core?.invoke;
|
||||
if (!tauri || !invoke) {
|
||||
throw new Error('Tauri invoke is not configured');
|
||||
}
|
||||
window.__TAURI__ = {
|
||||
...window.__TAURI__,
|
||||
installTauriRuntime({
|
||||
...tauri,
|
||||
core: {
|
||||
...window.__TAURI__.core,
|
||||
invoke: async (command, args) =>
|
||||
...tauri.core,
|
||||
// 泛型 invoke 无法由非泛型包装函数精确表达:命中目录选择命令时固定返回路径,其余转发。
|
||||
invoke: (async (command: string, args?: Record<string, unknown>) =>
|
||||
command === 'pick_local_project_directory'
|
||||
? projectPath
|
||||
: invoke(command, args),
|
||||
: invoke(command, args)) as unknown as typeof invoke,
|
||||
},
|
||||
};
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: action }));
|
||||
}
|
||||
|
||||
@@ -787,13 +794,13 @@ function createProjectChatRuntimeHarness({
|
||||
manifestInvalidatedHandlers.delete(
|
||||
handler as unknown as ManifestInvalidatedHandler,
|
||||
);
|
||||
if (progressHandler === handler) {
|
||||
if ((progressHandler as unknown) === handler) {
|
||||
progressHandler = null;
|
||||
}
|
||||
if (designAgentUpdateHandler === handler) {
|
||||
if ((designAgentUpdateHandler as unknown) === handler) {
|
||||
designAgentUpdateHandler = null;
|
||||
}
|
||||
if (directThreadNotifyHandler === handler) {
|
||||
if ((directThreadNotifyHandler as unknown) === handler) {
|
||||
directThreadNotifyHandler = null;
|
||||
}
|
||||
};
|
||||
@@ -933,6 +940,14 @@ function createProjectChatRuntimeHarness({
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装测试用 Tauri 替身。
|
||||
*
|
||||
* 定义已收进 `tests/tauriRuntimeFake.ts`(全仓唯一安装点),这里只转出,供 appSurface 各
|
||||
* suite 沿用既有的 `./harness` 导入路径。
|
||||
*/
|
||||
export { createTauriEventBridge, installTauriRuntime, resetTauriRuntime };
|
||||
|
||||
export function installResizeObserverStub() {
|
||||
let observerCount = 0;
|
||||
let observerDisconnected = false;
|
||||
@@ -1000,7 +1015,7 @@ afterEach(() => {
|
||||
window.history.pushState({}, '', '/');
|
||||
window.localStorage.clear();
|
||||
window.sessionStorage.clear();
|
||||
delete window.__TAURI__;
|
||||
resetTauriRuntime();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
if (originalCreateObjectUrl) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
findResourceSelectButton,
|
||||
fireEvent,
|
||||
getResourceSelectButton,
|
||||
installTauriRuntime,
|
||||
it,
|
||||
nativeClipboardMock,
|
||||
pickProjectFromLauncher,
|
||||
@@ -28,6 +29,12 @@ import {
|
||||
within,
|
||||
} from './harness';
|
||||
|
||||
// App 的 props 参数带默认值,`React.ComponentProps<typeof App>` 会带上 undefined,
|
||||
// createElement 因此推断不出 props;这里收窄成非空组件类型,运行时仍是同一个组件。
|
||||
const TestApp = App as React.ComponentType<
|
||||
NonNullable<React.ComponentProps<typeof App>>
|
||||
>;
|
||||
|
||||
/**
|
||||
* 外壳级浮层提示的挂点。
|
||||
*
|
||||
@@ -126,7 +133,7 @@ export function registerClientHomeTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
// 挂载即加载目录,触发按钮直接落在默认模型上,不出现「选择模型」空态。
|
||||
@@ -174,7 +181,7 @@ export function registerClientHomeTests() {
|
||||
if (command === 'read_game_creator_app_config') return { config: {} };
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
await waitFor(() =>
|
||||
expect(invoke).toHaveBeenCalledWith('get_game_template_library_access'),
|
||||
@@ -256,7 +263,7 @@ export function registerClientHomeTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
// 首页推荐位只展示封面、标题、运行时与已下载徽标,本机灵感图库已随模板库上线删除。
|
||||
@@ -308,7 +315,7 @@ export function registerClientHomeTests() {
|
||||
assets: [
|
||||
{
|
||||
id: 'live-hero',
|
||||
kind: 'icon-spritesheet',
|
||||
kind: 'icon-spritesheet' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/live-hero.png',
|
||||
source: { kind: 'generated' as const, taskId: 'art-asset-plan' },
|
||||
@@ -398,7 +405,7 @@ export function registerClientHomeTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
render(
|
||||
React.createElement(WorkspaceLauncherShell, {
|
||||
currentUser: testAuthUser,
|
||||
@@ -540,10 +547,10 @@ export function registerClientHomeTests() {
|
||||
return runtimeHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: runtimeHarness.listen },
|
||||
};
|
||||
});
|
||||
render(
|
||||
React.createElement(WorkspaceLauncherShell, {
|
||||
currentUser: testAuthUser,
|
||||
@@ -713,10 +720,10 @@ export function registerClientHomeTests() {
|
||||
return runtimeHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: runtimeHarness.listen },
|
||||
};
|
||||
});
|
||||
render(
|
||||
React.createElement(WorkspaceLauncherShell, {
|
||||
currentUser: testAuthUser,
|
||||
@@ -820,7 +827,7 @@ export function registerClientHomeTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
expect(screen.getByLabelText('GameAgent 客户端')).not.toBeNull();
|
||||
@@ -1017,7 +1024,7 @@ export function registerClientHomeTests() {
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
|
||||
renderLauncherAt('/?launcher');
|
||||
fireEvent.click(screen.getByRole('button', { name: '充值' }));
|
||||
@@ -1117,7 +1124,7 @@ export function registerClientHomeTests() {
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '我的' }));
|
||||
@@ -1166,7 +1173,9 @@ export function registerClientHomeTests() {
|
||||
|
||||
export function registerHomeProjectCreationTests() {
|
||||
it('scopes the 策划补全 option to the game entry and clears it on creation-type switch', () => {
|
||||
window.__TAURI__ = { core: { invoke: vi.fn(async () => null) } };
|
||||
installTauriRuntime({
|
||||
core: { invoke: vi.fn(async () => null) },
|
||||
});
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
const creationTypes = screen.getByRole('group', { name: '创作类型' });
|
||||
@@ -1223,10 +1232,10 @@ export function registerHomeProjectCreationTests() {
|
||||
return chatHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
};
|
||||
});
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: '策划补全' }));
|
||||
@@ -1279,7 +1288,7 @@ export function registerHomeProjectCreationTests() {
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify(['/tmp/authorized-game']),
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
expect(await screen.findByText('authorized-game')).not.toBeNull();
|
||||
@@ -1329,7 +1338,7 @@ export function registerHomeProjectCreationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
pickProjectFromLauncher('/tmp/missing-game');
|
||||
@@ -1371,7 +1380,7 @@ export function registerHomeProjectCreationTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
@@ -1424,7 +1433,7 @@ export function registerHomeProjectCreationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开项目' }));
|
||||
@@ -1457,7 +1466,7 @@ export function registerHomeProjectCreationTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开项目' }));
|
||||
@@ -1477,7 +1486,7 @@ export function registerHomeProjectCreationTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
const openButton = screen.getByRole('button', { name: '打开项目' });
|
||||
@@ -1536,10 +1545,10 @@ export function registerHomeProjectCreationTests() {
|
||||
return chatHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
};
|
||||
});
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
const creationTypes = screen.getByRole('group', { name: '创作类型' });
|
||||
@@ -1683,10 +1692,10 @@ export function registerHomeProjectCreationTests() {
|
||||
return chatHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
};
|
||||
});
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
const fileInput =
|
||||
@@ -1796,7 +1805,7 @@ export function registerHomeProjectCreationTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
const promptInput = screen.getByLabelText('创作想法');
|
||||
@@ -1868,7 +1877,7 @@ export function registerHomeProjectCreationTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
@@ -1962,10 +1971,10 @@ export function registerHomeProjectCreationTests() {
|
||||
return chatHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
};
|
||||
});
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
// 做方案走立项链路:建项之后还要写策划运行时,这一步失败时项目目录已经存在。
|
||||
@@ -2029,10 +2038,10 @@ export function registerHomeProjectCreationTests() {
|
||||
return chatHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
};
|
||||
});
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
if (modeLabel === '做方案') {
|
||||
@@ -2126,10 +2135,10 @@ export function registerHomeProjectCreationTests() {
|
||||
return chatHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
};
|
||||
});
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '做方案' }));
|
||||
@@ -2264,10 +2273,10 @@ export function registerHomeProjectCreationTests() {
|
||||
return chatHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
};
|
||||
});
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '做方案' }));
|
||||
@@ -2330,10 +2339,10 @@ export function registerHomeProjectCreationTests() {
|
||||
return runtimeHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: runtimeHarness.listen },
|
||||
};
|
||||
});
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
const promptInput = screen.getByLabelText('创作想法');
|
||||
@@ -2360,7 +2369,7 @@ export function registerHomeProjectCreationTests() {
|
||||
const committedAssets = [
|
||||
{
|
||||
id: 'direct-art-spec',
|
||||
kind: 'icon-spec',
|
||||
kind: 'icon-spec' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/art-spec.png',
|
||||
source: {
|
||||
@@ -2370,7 +2379,7 @@ export function registerHomeProjectCreationTests() {
|
||||
},
|
||||
{
|
||||
id: 'direct-game-background',
|
||||
kind: 'scene',
|
||||
kind: 'scene' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/direct-game-background.png',
|
||||
source: {
|
||||
@@ -2380,7 +2389,7 @@ export function registerHomeProjectCreationTests() {
|
||||
},
|
||||
{
|
||||
id: 'direct-art-spritesheet',
|
||||
kind: 'icon-spritesheet',
|
||||
kind: 'icon-spritesheet' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/art-spritesheet.png',
|
||||
source: {
|
||||
@@ -2506,10 +2515,10 @@ export function registerHomeProjectCreationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(TestApp, {
|
||||
initialProjectPath: projectPath,
|
||||
initialProjectManifest: manifest,
|
||||
initialPlanningPrompt: '继续修改已有项目',
|
||||
@@ -2590,10 +2599,10 @@ export function registerHomeProjectCreationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(TestApp, {
|
||||
initialProjectPath: projectPath,
|
||||
initialProjectManifest: manifest,
|
||||
initialPlanningPrompt: '生成一个游戏',
|
||||
@@ -2624,7 +2633,7 @@ export function registerHomeProjectCreationTests() {
|
||||
|
||||
// 重进项目:被拒消息不得从历史里被重放,拒绝文案也不该被当成历史条目。
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(TestApp, {
|
||||
initialProjectPath: projectPath,
|
||||
initialProjectManifest: manifest,
|
||||
}),
|
||||
@@ -2701,7 +2710,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
window.localStorage.setItem(
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify(paths),
|
||||
@@ -2772,7 +2781,7 @@ export function registerRecentProjectsTests() {
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
expect(screen.getByLabelText('最近项目')).not.toBeNull();
|
||||
@@ -2831,7 +2840,11 @@ export function registerRecentProjectsTests() {
|
||||
'星轨夜航',
|
||||
);
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
// 回调体内要读 invoke.mock.calls,显式标注返回类型以打断类型推断的自引用环。
|
||||
async (
|
||||
command: string,
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<unknown> => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
const renamed =
|
||||
@@ -2858,7 +2871,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
window.localStorage.setItem(
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify([projectPath]),
|
||||
@@ -2906,7 +2919,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
window.localStorage.setItem(
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify([projectPath]),
|
||||
@@ -2955,7 +2968,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
window.localStorage.setItem(
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify(['/tmp/recent-one']),
|
||||
@@ -2994,7 +3007,7 @@ export function registerRecentProjectsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
pickProjectFromLauncher('/tmp/open-failed-game');
|
||||
@@ -3044,7 +3057,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
window.localStorage.setItem(
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify([
|
||||
@@ -3191,7 +3204,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
window.localStorage.setItem(
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify(['/tmp/refreshable-game']),
|
||||
@@ -3256,7 +3269,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
expect(screen.getByLabelText('最近项目')).not.toBeNull();
|
||||
@@ -3304,7 +3317,7 @@ export function registerRecentProjectsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开项目' }));
|
||||
@@ -3334,7 +3347,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
|
||||
@@ -3375,7 +3388,7 @@ export function registerRecentProjectsTests() {
|
||||
},
|
||||
);
|
||||
const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
|
||||
@@ -3421,7 +3434,7 @@ export function registerRecentProjectsTests() {
|
||||
},
|
||||
);
|
||||
const confirm = vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
|
||||
@@ -3474,7 +3487,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
|
||||
@@ -3501,7 +3514,7 @@ export function registerRecentProjectsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
|
||||
@@ -3568,10 +3581,10 @@ export function registerRecentProjectsTests() {
|
||||
return supervisorHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: supervisorHarness.listen },
|
||||
};
|
||||
});
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
// 设置 → 工作区:默认位置就是不选目录,仍然落在 AGC 管理的应用数据目录。
|
||||
|
||||
@@ -10,9 +10,11 @@ import {
|
||||
emptyProjectPolicy,
|
||||
expect,
|
||||
fireEvent,
|
||||
installTauriRuntime,
|
||||
it,
|
||||
mockRoleAgentReply,
|
||||
renderAppAt,
|
||||
resetTauriRuntime,
|
||||
roleAgentMockReply,
|
||||
screen,
|
||||
vi,
|
||||
@@ -192,7 +194,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
invoke.mockClear();
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
@@ -266,7 +268,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
invoke.mockClear();
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
@@ -352,7 +354,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
invoke.mockClear();
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
@@ -428,7 +430,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const input = await screen.findByLabelText('Agent 对话内容');
|
||||
@@ -520,7 +522,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const input = await screen.findByLabelText('Agent 对话内容');
|
||||
@@ -616,7 +618,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
|
||||
@@ -685,12 +687,12 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const input = await screen.findByLabelText('Agent 对话内容');
|
||||
fireEvent.change(input, { target: { value: '确认运行环境提示' } });
|
||||
delete window.__TAURI__;
|
||||
resetTauriRuntime();
|
||||
fireEvent.submit(input.closest('form') as HTMLFormElement);
|
||||
|
||||
expect(await screen.findByText('需要在 Tauri App 内运行')).not.toBeNull();
|
||||
@@ -787,7 +789,7 @@ export function registerProjectConversationTests() {
|
||||
}
|
||||
throw new Error(`unexpected listen ${eventName}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke }, event: { listen } };
|
||||
installTauriRuntime({ core: { invoke }, event: { listen } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const input = await screen.findByLabelText('Agent 对话内容');
|
||||
@@ -876,7 +878,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
expect(await screen.findByText('旧 Agent 历史消息')).not.toBeNull();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import {
|
||||
fireEvent,
|
||||
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
||||
type GameCreationAgentRunTrace,
|
||||
installTauriRuntime,
|
||||
it,
|
||||
renderLauncherAt,
|
||||
screen,
|
||||
@@ -111,6 +112,9 @@ export function registerAgentStatusDerivationTests() {
|
||||
pass: 1,
|
||||
agent: 'Evaluator',
|
||||
phase: 'evaluation',
|
||||
taskId: null,
|
||||
group: null,
|
||||
role: null,
|
||||
status: 'passed',
|
||||
inputPaths: ['.agent/passes/pass-1/draft.json', '.agent/findings.md'],
|
||||
outputPaths: ['.agent/findings.md'],
|
||||
@@ -119,11 +123,14 @@ export function registerAgentStatusDerivationTests() {
|
||||
{
|
||||
toolId: 'agent.evaluate',
|
||||
status: 'ok',
|
||||
inputPaths: [],
|
||||
outputPaths: [],
|
||||
summary: 'Evaluator 质量评审',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
artifacts: [],
|
||||
taskGraph: {
|
||||
goal: '做一个厨房弹幕游戏',
|
||||
readyTaskIds: ['audio-director'],
|
||||
@@ -220,7 +227,7 @@ export function registerRuntimeSettingsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -269,7 +276,7 @@ export function registerRuntimeSettingsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -338,7 +345,7 @@ export function registerRuntimeSettingsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -412,7 +419,7 @@ export function registerRuntimeSettingsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -484,7 +491,7 @@ export function registerRuntimeSettingsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -536,7 +543,7 @@ export function registerRuntimeSettingsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -617,7 +624,7 @@ export function registerRuntimeSettingsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
document.documentElement.style.overflow = 'auto';
|
||||
document.body.style.overflow = 'scroll';
|
||||
renderLauncherAt('/?launcher');
|
||||
@@ -659,7 +666,7 @@ export function registerRuntimeSettingsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -715,7 +722,7 @@ export function registerRuntimeSettingsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
// 首页模型选择器挂载后会在目录请求之后读取一次配置,等它完成再打开对话框。
|
||||
|
||||
@@ -58,8 +58,8 @@ export function registerToolCallGroupTests() {
|
||||
'running',
|
||||
'completed',
|
||||
]);
|
||||
expect(within(rows[0]).getByText('失败')).not.toBeNull();
|
||||
expect(within(rows[2]).queryByText('失败')).toBeNull();
|
||||
expect(within(rows[0]!).getByText('失败')).not.toBeNull();
|
||||
expect(within(rows[2]!).queryByText('失败')).toBeNull();
|
||||
});
|
||||
|
||||
it('summarizes the group as its own operation count', () => {
|
||||
@@ -458,9 +458,9 @@ export function registerToolCallGroupTests() {
|
||||
view.rerender(
|
||||
React.createElement(ToolCallGroup, {
|
||||
calls: [
|
||||
{ ...calls[0], status: 'completed' as const, updatedAt: lastEnd },
|
||||
{ ...calls[0]!, status: 'completed' as const, updatedAt: lastEnd },
|
||||
{
|
||||
...calls[1],
|
||||
...calls[1]!,
|
||||
status: 'completed' as const,
|
||||
updatedAt: lastEnd - 400,
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import { AuthenticatedClient } from '../src/app/AuthenticatedClient';
|
||||
import { resetPlatformSessionForTests } from '../src/services/platformSession';
|
||||
import { installTauriRuntime, resetTauriRuntime } from './tauriRuntimeFake';
|
||||
import { installUnhandledRejectionBridge } from './unhandledRejectionBridge';
|
||||
|
||||
type AuthInvoke = (
|
||||
@@ -42,11 +43,12 @@ function reportCalls() {
|
||||
}
|
||||
|
||||
function renderLogin(invokeAuth: AuthInvoke) {
|
||||
window.__TAURI__ = { core: { invoke: invokeAuth } } as never;
|
||||
installTauriRuntime({ core: { invoke: invokeAuth } });
|
||||
render(
|
||||
createElement(AuthenticatedClient, null, () =>
|
||||
createElement('main', { 'aria-label': '已登录' }),
|
||||
),
|
||||
// React 19 的 createElement 的 children 形参只接受 ReactNode,渲染函数子节点改编进 props。
|
||||
createElement(AuthenticatedClient, {
|
||||
children: () => createElement('main', { 'aria-label': '已登录' }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,7 +69,7 @@ describe('认证失败的上报判据', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
resetPlatformSessionForTests();
|
||||
delete window.__TAURI__;
|
||||
resetTauriRuntime();
|
||||
vi.mocked(invoke).mockClear();
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useDirectProjectChatController,
|
||||
} from '../src/view/project-development/chat/controller/useDirectProjectChatController';
|
||||
import type { DirectCodexTurnAttachment } from '../src/view/project-development/chat/conversation/directCodexTurnAttachments';
|
||||
import { installTauriRuntime, resetTauriRuntime } from './tauriRuntimeFake';
|
||||
|
||||
// jsdom 的 Blob/File 没实现 `arrayBuffer`,而附件导入链路要按字节读文件。
|
||||
if (typeof Blob.prototype.arrayBuffer !== 'function') {
|
||||
@@ -25,7 +26,7 @@ function files(count: number, prefix: string) {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
window.__TAURI__ = undefined;
|
||||
resetTauriRuntime();
|
||||
});
|
||||
|
||||
describe('聊天输入盒的附件上限', () => {
|
||||
@@ -38,7 +39,7 @@ describe('聊天输入盒的附件上限', () => {
|
||||
const fileName = (args as { fileName?: string }).fileName ?? 'file';
|
||||
return { localPath: `assets/upload-${(uploadSeq += 1)}-${fileName}` };
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke: invoke as never } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDirectProjectChatController({
|
||||
@@ -65,7 +66,7 @@ describe('聊天输入盒的附件上限', () => {
|
||||
});
|
||||
expect(imported[0]).toHaveLength(5);
|
||||
expect(imported[1]).toHaveLength(3);
|
||||
expect(imported[0].length + imported[1].length).toBe(
|
||||
expect(imported[0]!.length + imported[1]!.length).toBe(
|
||||
MAX_CHAT_COMPOSER_ATTACHMENTS,
|
||||
);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user