修复AGC Native测试认证与阻断
- 为画布与资源编辑 loopback fixture 增加 task-local External Editor 凭据 - 为平台账号语义测试安装隔离会话并修正资源编辑错误分类断言 - 为 Windows mock listener 增加有界 accept 与 blocking stream 恢复 - 同步 autonomous main-loop、completion fixture 与项目决策记录
This commit is contained in:
@@ -6165,13 +6165,22 @@ mod canvas_generation_tests {
|
||||
})).collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let slices = prepare_platform_art_spritesheet_slices(
|
||||
&reqwest::Client::new(),
|
||||
&base_url,
|
||||
"test-api-key",
|
||||
&generated,
|
||||
0,
|
||||
0,
|
||||
let slices = crate::assets::with_external_editor_api_credentials(
|
||||
crate::assets::external_editor_api_credentials_for_test(
|
||||
base_url.clone(),
|
||||
"test-api-key".to_string(),
|
||||
),
|
||||
async {
|
||||
prepare_platform_art_spritesheet_slices(
|
||||
&reqwest::Client::new(),
|
||||
&base_url,
|
||||
"test-api-key",
|
||||
&generated,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("prepare external icon slices");
|
||||
@@ -6367,13 +6376,29 @@ mod canvas_generation_tests {
|
||||
|
||||
let listener =
|
||||
std::net::TcpListener::bind("127.0.0.1:0").expect("bind prepared recovery fixture");
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.expect("set prepared recovery fixture nonblocking");
|
||||
let base_url = format!("http://{}", listener.local_addr().expect("fixture address"));
|
||||
let server_base_url = base_url.clone();
|
||||
let png = rgba_test_png(u8::MAX).bytes;
|
||||
let (request_sender, request_receiver) = std::sync::mpsc::channel();
|
||||
let server = std::thread::spawn(move || {
|
||||
for request_index in 0..4 {
|
||||
let (mut stream, _) = listener.accept().expect("accept prepared recovery request");
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(5);
|
||||
let (mut stream, _) = loop {
|
||||
match listener.accept() {
|
||||
Ok(accepted) => break accepted,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"timed out waiting for prepared recovery request {request_index}"
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
Err(error) => panic!("accept prepared recovery request: {error}"),
|
||||
}
|
||||
};
|
||||
let request = read_test_http_request(&mut stream);
|
||||
request_sender
|
||||
.send(request)
|
||||
@@ -6434,15 +6459,6 @@ mod canvas_generation_tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
serde_json::json!({
|
||||
"editorApi": {
|
||||
"baseUrl": base_url,
|
||||
"apiKey": "prepared-recovery-key"
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
let runtime_context = PlatformArtGenerationRuntimeContext {
|
||||
agent_id: "art-director".to_string(),
|
||||
task_id: "art-director".to_string(),
|
||||
@@ -6480,27 +6496,36 @@ mod canvas_generation_tests {
|
||||
.timeout(Duration::from_secs(2))
|
||||
.build()
|
||||
.expect("build first submit client");
|
||||
let first_error = submit_external_generation_request(
|
||||
&first_submit_client,
|
||||
&base_url,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"prepared-recovery-key",
|
||||
&stable_key,
|
||||
&stable_body,
|
||||
let (first_error, prepared) = crate::assets::with_external_editor_api_credentials(
|
||||
crate::assets::external_editor_api_credentials_for_test(
|
||||
base_url.clone(),
|
||||
"prepared-recovery-key".to_string(),
|
||||
),
|
||||
async {
|
||||
let first_error = submit_external_generation_request(
|
||||
&first_submit_client,
|
||||
&base_url,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"prepared-recovery-key",
|
||||
&stable_key,
|
||||
&stable_body,
|
||||
)
|
||||
.await
|
||||
.expect_err("first response is intentionally lost");
|
||||
let prepared = request_platform_art_asset_with_runtime_options_at(
|
||||
root,
|
||||
"恢复时不得重建这个提示词",
|
||||
&[],
|
||||
&PlatformArtAssetGenerationOptions::default(),
|
||||
Some(&runtime_context),
|
||||
)
|
||||
.await
|
||||
.expect("resume prepared generation with the durable request");
|
||||
(first_error, prepared)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect_err("first response is intentionally lost");
|
||||
.await;
|
||||
assert!(platform_art_generation_error_result_unknown(&first_error));
|
||||
|
||||
let prepared = request_platform_art_asset_with_runtime_options_at(
|
||||
root,
|
||||
"恢复时不得重建这个提示词",
|
||||
&[],
|
||||
&PlatformArtAssetGenerationOptions::default(),
|
||||
Some(&runtime_context),
|
||||
)
|
||||
.await
|
||||
.expect("resume prepared generation with the durable request");
|
||||
server.join().expect("join prepared recovery fixture");
|
||||
assert_eq!(
|
||||
prepared.canvas_context.project_id,
|
||||
@@ -6595,14 +6620,23 @@ mod canvas_generation_tests {
|
||||
.timeout(Duration::from_secs(2))
|
||||
.build()
|
||||
.expect("build prepared auth client");
|
||||
let error = resume_prepared_external_generation_at(
|
||||
root,
|
||||
&client,
|
||||
&client,
|
||||
&base_url,
|
||||
"prepared-auth-key",
|
||||
"/api/external/v1/editor/images/generations",
|
||||
state,
|
||||
let error = crate::assets::with_external_editor_api_credentials(
|
||||
crate::assets::external_editor_api_credentials_for_test(
|
||||
base_url.clone(),
|
||||
"prepared-auth-key".to_string(),
|
||||
),
|
||||
async {
|
||||
resume_prepared_external_generation_at(
|
||||
root,
|
||||
&client,
|
||||
&client,
|
||||
&base_url,
|
||||
"prepared-auth-key",
|
||||
"/api/external/v1/editor/images/generations",
|
||||
state,
|
||||
)
|
||||
.await
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect_err("auth rejection cannot prove the original request was not accepted");
|
||||
@@ -6678,17 +6712,26 @@ mod canvas_generation_tests {
|
||||
.timeout(Duration::from_secs(2))
|
||||
.build()
|
||||
.expect("build polling client");
|
||||
let result = wait_for_external_generation_result(
|
||||
&client,
|
||||
&base_url,
|
||||
"test-api-key",
|
||||
&serde_json::json!({
|
||||
"data": {
|
||||
"operationId": "task-async",
|
||||
"status": "queued",
|
||||
"pollAfterMs": 0
|
||||
}
|
||||
}),
|
||||
let result = crate::assets::with_external_editor_api_credentials(
|
||||
crate::assets::external_editor_api_credentials_for_test(
|
||||
base_url.clone(),
|
||||
"test-api-key".to_string(),
|
||||
),
|
||||
async {
|
||||
wait_for_external_generation_result(
|
||||
&client,
|
||||
&base_url,
|
||||
"test-api-key",
|
||||
&serde_json::json!({
|
||||
"data": {
|
||||
"operationId": "task-async",
|
||||
"status": "queued",
|
||||
"pollAfterMs": 0
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("poll completed result");
|
||||
@@ -6923,15 +6966,6 @@ mod canvas_generation_tests {
|
||||
.expect("set configuration drift fixture nonblocking");
|
||||
let current_base_url =
|
||||
format!("http://{}", listener.local_addr().expect("fixture address"));
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
serde_json::json!({
|
||||
"editorApi": {
|
||||
"baseUrl": current_base_url,
|
||||
"apiKey": "current-editor-key"
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
let runtime_context = PlatformArtGenerationRuntimeContext {
|
||||
agent_id: "art-director".to_string(),
|
||||
task_id: "art-director".to_string(),
|
||||
@@ -6962,18 +6996,27 @@ mod canvas_generation_tests {
|
||||
mark_platform_art_generation_runtime_accepted(root, state, "stale-operation", 0)
|
||||
.expect("mark stale operation accepted");
|
||||
|
||||
let error = match request_platform_art_asset_with_runtime_options_at(
|
||||
root,
|
||||
"不得覆盖旧请求",
|
||||
&[],
|
||||
&PlatformArtAssetGenerationOptions::default(),
|
||||
Some(&runtime_context),
|
||||
let error = crate::assets::with_external_editor_api_credentials(
|
||||
crate::assets::external_editor_api_credentials_for_test(
|
||||
current_base_url,
|
||||
"current-editor-key".to_string(),
|
||||
),
|
||||
async {
|
||||
match request_platform_art_asset_with_runtime_options_at(
|
||||
root,
|
||||
"不得覆盖旧请求",
|
||||
&[],
|
||||
&PlatformArtAssetGenerationOptions::default(),
|
||||
Some(&runtime_context),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(error) => error,
|
||||
Ok(_) => panic!("configuration drift must block GET-only recovery"),
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(error) => error,
|
||||
Ok(_) => panic!("configuration drift must block GET-only recovery"),
|
||||
};
|
||||
.await;
|
||||
assert!(error.contains("服务地址身份"), "{error}");
|
||||
assert!(matches!(
|
||||
listener.accept(),
|
||||
@@ -7003,10 +7046,26 @@ mod canvas_generation_tests {
|
||||
.expect("allow generation recovery");
|
||||
let listener =
|
||||
std::net::TcpListener::bind("127.0.0.1:0").expect("bind accepted failure fixture");
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.expect("set accepted failure fixture nonblocking");
|
||||
let base_url = format!("http://{}", listener.local_addr().expect("fixture address"));
|
||||
let (request_sender, request_receiver) = std::sync::mpsc::channel();
|
||||
let server = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept failed operation GET");
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(2);
|
||||
let (mut stream, _) = loop {
|
||||
match listener.accept() {
|
||||
Ok(accepted) => break accepted,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"timed out waiting for failed operation GET"
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
Err(error) => panic!("accept failed operation GET: {error}"),
|
||||
}
|
||||
};
|
||||
let request = read_test_http_request(&mut stream);
|
||||
request_sender
|
||||
.send(request)
|
||||
@@ -7028,12 +7087,6 @@ mod canvas_generation_tests {
|
||||
.write_all(response.as_bytes())
|
||||
.expect("write failed operation response");
|
||||
});
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
serde_json::json!({
|
||||
"editorApi": { "baseUrl": base_url.clone(), "apiKey": "accepted-failure-key" }
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
let runtime_context = PlatformArtGenerationRuntimeContext {
|
||||
agent_id: "art-director".to_string(),
|
||||
task_id: "art-director".to_string(),
|
||||
@@ -7066,18 +7119,27 @@ mod canvas_generation_tests {
|
||||
mark_platform_art_generation_runtime_accepted(root, state, "failed-operation", 0)
|
||||
.expect("mark failed operation accepted");
|
||||
|
||||
let error = match request_platform_art_asset_with_runtime_options_at(
|
||||
root,
|
||||
"不得重新提交",
|
||||
&[],
|
||||
&PlatformArtAssetGenerationOptions::default(),
|
||||
Some(&runtime_context),
|
||||
let error = crate::assets::with_external_editor_api_credentials(
|
||||
crate::assets::external_editor_api_credentials_for_test(
|
||||
base_url,
|
||||
"accepted-failure-key".to_string(),
|
||||
),
|
||||
async {
|
||||
match request_platform_art_asset_with_runtime_options_at(
|
||||
root,
|
||||
"不得重新提交",
|
||||
&[],
|
||||
&PlatformArtAssetGenerationOptions::default(),
|
||||
Some(&runtime_context),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(error) => error,
|
||||
Ok(_) => panic!("explicit operation failure must be returned"),
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(error) => error,
|
||||
Ok(_) => panic!("explicit operation failure must be returned"),
|
||||
};
|
||||
.await;
|
||||
server.join().expect("join accepted failure fixture");
|
||||
assert!(error.contains("平台图片生成任务失败"), "{error}");
|
||||
assert!(request_receiver
|
||||
@@ -7133,7 +7195,7 @@ mod canvas_generation_tests {
|
||||
request_sender
|
||||
.send(request.clone())
|
||||
.expect("capture accepted scan request");
|
||||
if request.starts_with("GET /api/external/v1/generations/test-operation-id ") {
|
||||
if request.starts_with("GET /api/runtime/external-generation/jobs/test-operation-id ") {
|
||||
let body = serde_json::json!({
|
||||
"data": {
|
||||
"operationId": "test-operation-id",
|
||||
@@ -7178,7 +7240,6 @@ mod canvas_generation_tests {
|
||||
]);
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
serde_json::json!({
|
||||
"editorApi": { "baseUrl": base_url, "apiKey": "recovery-editor-key" },
|
||||
"agentLlm": {
|
||||
"art-director": {
|
||||
"apiKey": "recovery-llm-key",
|
||||
@@ -7190,6 +7251,11 @@ mod canvas_generation_tests {
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
||||
"accepted-recovery-scan-user",
|
||||
"recovery-editor-key",
|
||||
&base_url,
|
||||
);
|
||||
let run_id = "accepted-recovery-scan-run";
|
||||
setup_platform_art_generation_runtime_accepted_for_recovery_test(root, run_id)
|
||||
.expect("setup accepted generation recovery state");
|
||||
@@ -7217,7 +7283,7 @@ mod canvas_generation_tests {
|
||||
stop_sender.send(()).expect("stop accepted scan fixture");
|
||||
server.join().expect("join accepted scan fixture");
|
||||
|
||||
assert!(first.starts_with("GET /api/external/v1/generations/test-operation-id "));
|
||||
assert!(first.starts_with("GET /api/runtime/external-generation/jobs/test-operation-id "));
|
||||
assert!(second.starts_with("GET /artifact.png "));
|
||||
assert!(request_receiver
|
||||
.recv_timeout(Duration::from_millis(100))
|
||||
@@ -7287,11 +7353,24 @@ mod canvas_generation_tests {
|
||||
.timeout(Duration::from_secs(2))
|
||||
.build()
|
||||
.expect("build legacy download client");
|
||||
let download =
|
||||
resolve_canvas_resource_download(&client, &base_url, "test-api-key", &download_source)
|
||||
let download = crate::assets::with_external_editor_api_credentials(
|
||||
crate::assets::external_editor_api_credentials_for_test(
|
||||
base_url.clone(),
|
||||
"test-api-key".to_string(),
|
||||
),
|
||||
async {
|
||||
resolve_canvas_resource_download(
|
||||
&client,
|
||||
&base_url,
|
||||
"test-api-key",
|
||||
&download_source,
|
||||
)
|
||||
.await
|
||||
.expect("resolve legacy spritesheet download")
|
||||
.expect("legacy spritesheet download");
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("resolve legacy spritesheet download")
|
||||
.expect("legacy spritesheet download");
|
||||
assert_eq!(download.bytes, expected_bytes);
|
||||
assert_eq!(download.media_type, "image/png");
|
||||
let read_url_request = receiver
|
||||
|
||||
@@ -2291,8 +2291,10 @@ fn autonomous_visual_ready_tasks_only_require_images_when_editor_api_key_is_conf
|
||||
.expect_err("art-director without External Editor API must remain read-only");
|
||||
assert!(error.contains("自主构建只读专业 Agent 禁止执行写入或副作用动作"));
|
||||
}
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"visual-ready-task-test-key"}}"#.to_string(),
|
||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
||||
"visual-ready-task-user",
|
||||
"visual-ready-task-test-key",
|
||||
"https://dev.genarrative.world",
|
||||
);
|
||||
assert!(autonomous_manifest_ready_task_requires_visual_asset(
|
||||
"art-director"
|
||||
@@ -2322,8 +2324,10 @@ fn autonomous_visual_ready_tasks_only_require_images_when_editor_api_key_is_conf
|
||||
|
||||
#[test]
|
||||
fn game_chat_art_director_uses_deterministic_canvas_plan_without_provider_planning() {
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"game-chat-fast-art-key"}}"#.to_string(),
|
||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
||||
"game-chat-fast-art-user",
|
||||
"game-chat-fast-art-key",
|
||||
"https://dev.genarrative.world",
|
||||
);
|
||||
let temporary = tempfile::tempdir().expect("create game-chat art root");
|
||||
let root = temporary.path().join("project");
|
||||
@@ -2367,8 +2371,10 @@ fn game_chat_art_director_uses_deterministic_canvas_plan_without_provider_planni
|
||||
|
||||
#[test]
|
||||
fn game_chat_art_asset_plan_uses_deterministic_icon_spritesheet_generation() {
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"game-chat-fast-spritesheet-key"}}"#.to_string(),
|
||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
||||
"game-chat-fast-spritesheet-user",
|
||||
"game-chat-fast-spritesheet-key",
|
||||
"https://dev.genarrative.world",
|
||||
);
|
||||
let temporary = tempfile::tempdir().expect("create game-chat spritesheet root");
|
||||
let root = temporary.path().join("project");
|
||||
@@ -4503,8 +4509,10 @@ fn game_chat_scheduler_recovers_deterministic_active_code_prototype_with_v1_task
|
||||
const PARENT_RUN_ID: &str = "game-chat-v1-ready-text-recovery-parent";
|
||||
const PARENT_TASK: &str = "继续把已有美术接入俄罗斯方块";
|
||||
const MAIN_AGENT_ID: &str = "code-prototype";
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"game-chat-v1-task-recovery-key"}}"#.to_string(),
|
||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
||||
"game-chat-v1-task-recovery-user",
|
||||
"game-chat-v1-task-recovery-key",
|
||||
"https://dev.genarrative.world",
|
||||
);
|
||||
let temporary = tempfile::tempdir().expect("create v1 task recovery root");
|
||||
let root = temporary.path().join("project");
|
||||
@@ -4995,8 +5003,10 @@ async fn missing_completed_visual_asset_fails_same_child_without_retry() {
|
||||
const PARENT_RUN_ID: &str = "autonomous-visual-recovery-parent";
|
||||
const PARENT_TASK: &str = "生成完整小游戏并恢复丢失的正式视觉产物";
|
||||
const CHILD_ID: &str = "art-director";
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"visual-recovery-test-key"}}"#.to_string(),
|
||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
||||
"visual-recovery-user",
|
||||
"visual-recovery-test-key",
|
||||
"https://dev.genarrative.world",
|
||||
);
|
||||
let temporary = tempfile::tempdir().expect("create visual recovery root");
|
||||
let root = temporary.path().join("project");
|
||||
|
||||
+8
-4
@@ -6406,8 +6406,10 @@ fn canvas_visual_gate_rejects_dimensions_from_a_shadow_canvas_object() {
|
||||
|
||||
#[test]
|
||||
fn cli_code_prototype_keeps_registered_canvas_spritesheet_gate_when_editor_is_configured() {
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"cli-art-gate-key"}}"#.to_string(),
|
||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
||||
"cli-art-gate-user",
|
||||
"cli-art-gate-key",
|
||||
"https://dev.genarrative.world",
|
||||
);
|
||||
let (_temporary, root, parent_state, _contract) =
|
||||
autonomous_fixture("创建完整小游戏", "cli-code-art-gate-parent");
|
||||
@@ -6480,8 +6482,10 @@ fn cli_code_prototype_accepts_linked_inline_and_external_modules_for_canvas_atla
|
||||
|
||||
#[test]
|
||||
fn cli_code_prototype_rejects_unlinked_unreachable_modules_and_html_path_decoys() {
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"cli-module-decoy-key"}}"#.to_string(),
|
||||
let _platform_session = crate::platform_session::install_test_platform_session(
|
||||
"cli-module-decoy-user",
|
||||
"cli-module-decoy-key",
|
||||
"https://dev.genarrative.world",
|
||||
);
|
||||
let (_temporary, root, parent_state, _contract) =
|
||||
autonomous_fixture("创建完整小游戏", "cli-module-decoy-parent");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use std::future::Future;
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use std::future::Future;
|
||||
|
||||
const PRIVATE_EXTERNAL_EDITOR_API_KEY_FILE_PREFIX: &str = "external-editor-api-";
|
||||
const PRIVATE_EXTERNAL_EDITOR_API_KEY_MAX_BYTES: u64 = 8 * 1024;
|
||||
@@ -47,6 +47,17 @@ pub(crate) async fn with_external_editor_api_credentials<T>(
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn external_editor_api_credentials_for_test(
|
||||
api_base_url: String,
|
||||
api_key: String,
|
||||
) -> ExternalEditorApiCredentials {
|
||||
ExternalEditorApiCredentials {
|
||||
api_base_url,
|
||||
api_key,
|
||||
}
|
||||
}
|
||||
|
||||
fn private_external_editor_api_key_directory() -> Result<PathBuf, String> {
|
||||
let home = std::env::var_os("USERPROFILE")
|
||||
.or_else(|| std::env::var_os("HOME"))
|
||||
@@ -56,7 +67,9 @@ fn private_external_editor_api_key_directory() -> Result<PathBuf, String> {
|
||||
Ok(home.join(".config").join("genarrative"))
|
||||
}
|
||||
|
||||
fn private_external_editor_api_key_path_for_base_url(api_base_url: &str) -> Result<PathBuf, String> {
|
||||
fn private_external_editor_api_key_path_for_base_url(
|
||||
api_base_url: &str,
|
||||
) -> Result<PathBuf, String> {
|
||||
let api_base_url = normalize_external_editor_api_base_url(api_base_url)?;
|
||||
let fingerprint = format!("{:x}", Sha256::digest(api_base_url.as_bytes()));
|
||||
Ok(private_external_editor_api_key_directory()?.join(format!(
|
||||
@@ -1675,13 +1688,11 @@ mod tests {
|
||||
fn private_external_editor_credentials_are_isolated_by_server_origin() {
|
||||
let local = private_external_editor_api_key_path_for_base_url("http://127.0.0.1:8082")
|
||||
.expect("local credential path");
|
||||
let previous =
|
||||
private_external_editor_api_key_path_for_base_url("http://127.0.0.1:8085")
|
||||
.expect("previous local credential path");
|
||||
let dev = private_external_editor_api_key_path_for_base_url(
|
||||
"https://dev.genarrative.world",
|
||||
)
|
||||
.expect("dev credential path");
|
||||
let previous = private_external_editor_api_key_path_for_base_url("http://127.0.0.1:8085")
|
||||
.expect("previous local credential path");
|
||||
let dev =
|
||||
private_external_editor_api_key_path_for_base_url("https://dev.genarrative.world")
|
||||
.expect("dev credential path");
|
||||
|
||||
assert_ne!(local, previous);
|
||||
assert_ne!(local, dev);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4208,12 +4208,57 @@ pub(crate) async fn derive_local_project_resource_at(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::future::Future;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::sync::mpsc;
|
||||
|
||||
const PROJECT_ID: &str = "resource-editor-test-project";
|
||||
|
||||
async fn with_test_external_editor_credentials<T>(
|
||||
api_base_url: &str,
|
||||
api_key: &str,
|
||||
operation: impl Future<Output = T>,
|
||||
) -> T {
|
||||
crate::assets::with_external_editor_api_credentials(
|
||||
crate::assets::external_editor_api_credentials_for_test(
|
||||
api_base_url.to_string(),
|
||||
api_key.to_string(),
|
||||
),
|
||||
operation,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn accept_resource_editor_fixture_connection(
|
||||
listener: &TcpListener,
|
||||
fixture: &str,
|
||||
request_index: usize,
|
||||
) -> TcpStream {
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.expect("set resource editor fixture listener nonblocking");
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(5);
|
||||
loop {
|
||||
match listener.accept() {
|
||||
Ok((stream, _)) => {
|
||||
stream
|
||||
.set_nonblocking(false)
|
||||
.expect("restore resource editor fixture stream blocking mode");
|
||||
return stream;
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"timed out waiting for {fixture} request {request_index}"
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
Err(error) => panic!("accept {fixture} request {request_index}: {error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_http_request(stream: &mut TcpStream) -> String {
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
@@ -4693,16 +4738,24 @@ mod tests {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind auth fixture");
|
||||
let base_url = format!("http://{}", listener.local_addr().expect("auth address"));
|
||||
let server = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept auth request");
|
||||
let mut stream =
|
||||
accept_resource_editor_fixture_connection(&listener, "auth fixture", 0);
|
||||
let _ = read_http_request(&mut stream);
|
||||
write_json(&mut stream, status, serde_json::json!({"error": "denied"}));
|
||||
});
|
||||
let error =
|
||||
let error = with_test_external_editor_credentials(&base_url, "rotated-key", async {
|
||||
submit_resource_edit_remote(root, &client, &base_url, "rotated-key", &mut ledger)
|
||||
.await
|
||||
.expect_err("authentication status must fail");
|
||||
})
|
||||
.await
|
||||
.expect_err("authentication status must fail");
|
||||
server.join().expect("join auth fixture");
|
||||
assert!(error.contains("authentication-required"));
|
||||
let expected_code = if status.starts_with("401") {
|
||||
"authentication-required"
|
||||
} else {
|
||||
"permission-denied"
|
||||
};
|
||||
assert!(error.contains(expected_code), "{status}: {error}");
|
||||
assert_eq!(ledger.phase, ResourceEditLedgerPhase::Prepared, "{status}");
|
||||
assert!(ledger.remote_operation_id.is_none(), "{status}");
|
||||
}
|
||||
@@ -4717,17 +4770,25 @@ mod tests {
|
||||
listener.local_addr().expect("poll auth address")
|
||||
);
|
||||
let server = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept poll auth request");
|
||||
let mut stream =
|
||||
accept_resource_editor_fixture_connection(&listener, "poll auth fixture", 0);
|
||||
let request = read_http_request(&mut stream);
|
||||
assert!(request.starts_with("GET /api/external/v1/generations/remote-one "));
|
||||
write_json(&mut stream, status, serde_json::json!({"error": "denied"}));
|
||||
});
|
||||
let error =
|
||||
let error = with_test_external_editor_credentials(&base_url, "rotated-key", async {
|
||||
wait_for_resource_edit_remote(root, &client, &base_url, "rotated-key", &mut ledger)
|
||||
.await
|
||||
.expect_err("poll authentication status must fail");
|
||||
})
|
||||
.await
|
||||
.expect_err("poll authentication status must fail");
|
||||
server.join().expect("join poll auth fixture");
|
||||
assert!(error.contains("authentication-required"));
|
||||
let expected_code = if status.starts_with("401") {
|
||||
"authentication-required"
|
||||
} else {
|
||||
"permission-denied"
|
||||
};
|
||||
assert!(error.contains(expected_code), "{status}: {error}");
|
||||
assert_eq!(ledger.phase, ResourceEditLedgerPhase::Accepted, "{status}");
|
||||
assert_eq!(ledger.remote_operation_id.as_deref(), Some("remote-one"));
|
||||
}
|
||||
@@ -4738,7 +4799,8 @@ mod tests {
|
||||
listener.local_addr().expect("missing status address")
|
||||
);
|
||||
let server = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept status request");
|
||||
let mut stream =
|
||||
accept_resource_editor_fixture_connection(&listener, "missing status fixture", 0);
|
||||
let _ = read_http_request(&mut stream);
|
||||
write_json(
|
||||
&mut stream,
|
||||
@@ -4746,10 +4808,12 @@ mod tests {
|
||||
serde_json::json!({"error": "missing"}),
|
||||
);
|
||||
});
|
||||
let error =
|
||||
let error = with_test_external_editor_credentials(&base_url, "rotated-key", async {
|
||||
wait_for_resource_edit_remote(root, &client, &base_url, "rotated-key", &mut ledger)
|
||||
.await
|
||||
.expect_err("missing remote status must stay unknown");
|
||||
})
|
||||
.await
|
||||
.expect_err("missing remote status must stay unknown");
|
||||
server.join().expect("join missing status fixture");
|
||||
assert!(error.contains("result-unknown"));
|
||||
assert_eq!(ledger.phase, ResourceEditLedgerPhase::Accepted);
|
||||
@@ -4780,13 +4844,6 @@ mod tests {
|
||||
listener.local_addr().expect("service identity address")
|
||||
);
|
||||
let new_api_key = "rotated-resource-editor-key";
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
serde_json::json!({"editorApi": {
|
||||
"baseUrl": base_url,
|
||||
"apiKey": new_api_key
|
||||
}})
|
||||
.to_string(),
|
||||
);
|
||||
let request = input(
|
||||
root,
|
||||
Uuid::new_v4().to_string(),
|
||||
@@ -4816,13 +4873,16 @@ mod tests {
|
||||
ledger.remote_operation_id = Some("legacy-remote-operation".to_string());
|
||||
write_resource_edit_ledger(root, &ledger).expect("write legacy ledger");
|
||||
|
||||
let first = request_resource_edit_service_identity_confirmation_at(
|
||||
RequestResourceEditServiceIdentityConfirmationInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: PROJECT_ID.to_string(),
|
||||
operation_id: request.operation_id.clone(),
|
||||
},
|
||||
)
|
||||
let first = with_test_external_editor_credentials(&base_url, new_api_key, async {
|
||||
request_resource_edit_service_identity_confirmation_at(
|
||||
RequestResourceEditServiceIdentityConfirmationInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: PROJECT_ID.to_string(),
|
||||
operation_id: request.operation_id.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.expect("issue service identity challenge");
|
||||
assert_eq!(
|
||||
@@ -4836,7 +4896,7 @@ mod tests {
|
||||
.expect("challenged ledger");
|
||||
changed.request_body_json = Some("{\"prompt\":\"changed\"}".to_string());
|
||||
write_resource_edit_ledger(root, &changed).expect("change challenged snapshot");
|
||||
let stale_error =
|
||||
let stale_error = with_test_external_editor_credentials(&base_url, new_api_key, async {
|
||||
confirm_resource_edit_service_identity_at(ConfirmResourceEditServiceIdentityInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: PROJECT_ID.to_string(),
|
||||
@@ -4845,16 +4905,21 @@ mod tests {
|
||||
challenge: first.challenge,
|
||||
})
|
||||
.await
|
||||
.expect_err("changed ledger must invalidate challenge");
|
||||
})
|
||||
.await
|
||||
.expect_err("changed ledger must invalidate challenge");
|
||||
assert!(stale_error.contains("上下文已变化"), "{stale_error}");
|
||||
|
||||
let second = request_resource_edit_service_identity_confirmation_at(
|
||||
RequestResourceEditServiceIdentityConfirmationInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: PROJECT_ID.to_string(),
|
||||
operation_id: request.operation_id.clone(),
|
||||
},
|
||||
)
|
||||
let second = with_test_external_editor_credentials(&base_url, new_api_key, async {
|
||||
request_resource_edit_service_identity_confirmation_at(
|
||||
RequestResourceEditServiceIdentityConfirmationInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: PROJECT_ID.to_string(),
|
||||
operation_id: request.operation_id.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.expect("rotate stale challenge");
|
||||
let mut expired = read_resource_edit_ledger(root, &request.operation_id)
|
||||
@@ -4866,7 +4931,7 @@ mod tests {
|
||||
.expect("second private challenge")
|
||||
.expires_at = unix_timestamp();
|
||||
write_resource_edit_ledger(root, &expired).expect("expire service identity challenge");
|
||||
let expired_error =
|
||||
let expired_error = with_test_external_editor_credentials(&base_url, new_api_key, async {
|
||||
confirm_resource_edit_service_identity_at(ConfirmResourceEditServiceIdentityInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: PROJECT_ID.to_string(),
|
||||
@@ -4875,18 +4940,23 @@ mod tests {
|
||||
challenge: second.challenge,
|
||||
})
|
||||
.await
|
||||
.expect_err("expired challenge must fail closed");
|
||||
})
|
||||
.await
|
||||
.expect_err("expired challenge must fail closed");
|
||||
assert!(expired_error.contains("已过期"), "{expired_error}");
|
||||
let current = request_resource_edit_service_identity_confirmation_at(
|
||||
RequestResourceEditServiceIdentityConfirmationInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: PROJECT_ID.to_string(),
|
||||
operation_id: request.operation_id.clone(),
|
||||
},
|
||||
)
|
||||
let current = with_test_external_editor_credentials(&base_url, new_api_key, async {
|
||||
request_resource_edit_service_identity_confirmation_at(
|
||||
RequestResourceEditServiceIdentityConfirmationInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: PROJECT_ID.to_string(),
|
||||
operation_id: request.operation_id.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.expect("rotate expired challenge");
|
||||
let confirmed =
|
||||
let confirmed = with_test_external_editor_credentials(&base_url, new_api_key, async {
|
||||
confirm_resource_edit_service_identity_at(ConfirmResourceEditServiceIdentityInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: PROJECT_ID.to_string(),
|
||||
@@ -4895,14 +4965,17 @@ mod tests {
|
||||
challenge: current.challenge,
|
||||
})
|
||||
.await
|
||||
.expect("confirm current service identity");
|
||||
})
|
||||
.await
|
||||
.expect("confirm current service identity");
|
||||
assert_eq!(
|
||||
confirmed.identity_scheme,
|
||||
PLATFORM_ART_GENERATION_SERVICE_IDENTITY_SCHEME
|
||||
);
|
||||
|
||||
let server = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept resumed poll");
|
||||
let mut stream =
|
||||
accept_resource_editor_fixture_connection(&listener, "resumed poll fixture", 0);
|
||||
let request = read_http_request(&mut stream);
|
||||
assert!(
|
||||
request.starts_with("GET /api/external/v1/generations/legacy-remote-operation ")
|
||||
@@ -4923,13 +4996,16 @@ mod tests {
|
||||
let mut resumed = read_resource_edit_ledger(root, &request.operation_id)
|
||||
.expect("read confirmed ledger")
|
||||
.expect("confirmed ledger");
|
||||
wait_for_resource_edit_remote(
|
||||
root,
|
||||
&reqwest::Client::new(),
|
||||
&base_url,
|
||||
new_api_key,
|
||||
&mut resumed,
|
||||
)
|
||||
with_test_external_editor_credentials(&base_url, new_api_key, async {
|
||||
wait_for_resource_edit_remote(
|
||||
root,
|
||||
&reqwest::Client::new(),
|
||||
&base_url,
|
||||
new_api_key,
|
||||
&mut resumed,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.expect("resume accepted operation by GET");
|
||||
server.join().expect("join resumed poll server");
|
||||
@@ -4983,7 +5059,11 @@ mod tests {
|
||||
listener.local_addr().expect("submission address")
|
||||
);
|
||||
let server = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept submission request");
|
||||
let mut stream = accept_resource_editor_fixture_connection(
|
||||
&listener,
|
||||
"submission status fixture",
|
||||
0,
|
||||
);
|
||||
let request = read_http_request(&mut stream);
|
||||
assert!(request.starts_with("POST /api/external/v1/editor/videos/generations"));
|
||||
write_json(
|
||||
@@ -4992,13 +5072,16 @@ mod tests {
|
||||
serde_json::json!({"error": "provider detail must not persist"}),
|
||||
);
|
||||
});
|
||||
let error = submit_resource_edit_remote(
|
||||
root,
|
||||
&reqwest::Client::new(),
|
||||
&base_url,
|
||||
"rotated-key",
|
||||
&mut ledger,
|
||||
)
|
||||
let error = with_test_external_editor_credentials(&base_url, "rotated-key", async {
|
||||
submit_resource_edit_remote(
|
||||
root,
|
||||
&reqwest::Client::new(),
|
||||
&base_url,
|
||||
"rotated-key",
|
||||
&mut ledger,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.expect_err("submission status must fail");
|
||||
server.join().expect("join submission fixture");
|
||||
@@ -5099,7 +5182,11 @@ mod tests {
|
||||
listener.local_addr().expect("accepted response address")
|
||||
);
|
||||
let server = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept submission request");
|
||||
let mut stream = accept_resource_editor_fixture_connection(
|
||||
&listener,
|
||||
"accepted response fixture",
|
||||
0,
|
||||
);
|
||||
let request = read_http_request(&mut stream);
|
||||
assert!(request.starts_with("POST /api/external/v1/editor/videos/generations"));
|
||||
write_bytes(
|
||||
@@ -5109,13 +5196,16 @@ mod tests {
|
||||
response_body.as_bytes(),
|
||||
);
|
||||
});
|
||||
let error = submit_resource_edit_remote(
|
||||
root,
|
||||
&reqwest::Client::new(),
|
||||
&base_url,
|
||||
"rotated-key",
|
||||
&mut ledger,
|
||||
)
|
||||
let error = with_test_external_editor_credentials(&base_url, "rotated-key", async {
|
||||
submit_resource_edit_remote(
|
||||
root,
|
||||
&reqwest::Client::new(),
|
||||
&base_url,
|
||||
"rotated-key",
|
||||
&mut ledger,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.expect_err("unproven accepted response must fail closed");
|
||||
server.join().expect("join accepted fixture");
|
||||
@@ -5171,13 +5261,6 @@ mod tests {
|
||||
.expect("unavailable endpoint address")
|
||||
);
|
||||
drop(unavailable);
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
serde_json::json!({"editorApi": {
|
||||
"baseUrl": base_url,
|
||||
"apiKey": "transport-failure-key"
|
||||
}})
|
||||
.to_string(),
|
||||
);
|
||||
let mut request = input(
|
||||
root,
|
||||
Uuid::new_v4().to_string(),
|
||||
@@ -5189,7 +5272,10 @@ mod tests {
|
||||
request.source_subtype = Some("background-music".to_string());
|
||||
request.producer_task_id = Some(task_id);
|
||||
|
||||
let error = derive_local_project_resource_at(request.clone())
|
||||
let error =
|
||||
with_test_external_editor_credentials(&base_url, "transport-failure-key", async {
|
||||
derive_local_project_resource_at(request.clone()).await
|
||||
})
|
||||
.await
|
||||
.expect_err("transport failure must fail closed");
|
||||
assert!(error.contains("result-unknown"), "{error}");
|
||||
@@ -5247,7 +5333,8 @@ mod tests {
|
||||
listener.local_addr().expect("failed status address")
|
||||
);
|
||||
let server = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept failed status request");
|
||||
let mut stream =
|
||||
accept_resource_editor_fixture_connection(&listener, "failed status fixture", 0);
|
||||
let request = read_http_request(&mut stream);
|
||||
assert!(request.starts_with("GET /api/external/v1/generations/remote-terminal-failure"));
|
||||
write_json(
|
||||
@@ -5257,13 +5344,16 @@ mod tests {
|
||||
);
|
||||
});
|
||||
|
||||
let error = wait_for_resource_edit_remote(
|
||||
root,
|
||||
&reqwest::Client::new(),
|
||||
&base_url,
|
||||
"rotated-key",
|
||||
&mut ledger,
|
||||
)
|
||||
let error = with_test_external_editor_credentials(&base_url, "rotated-key", async {
|
||||
wait_for_resource_edit_remote(
|
||||
root,
|
||||
&reqwest::Client::new(),
|
||||
&base_url,
|
||||
"rotated-key",
|
||||
&mut ledger,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.expect_err("failed status must become terminal");
|
||||
server.join().expect("join failed status fixture");
|
||||
@@ -5489,8 +5579,12 @@ mod tests {
|
||||
let server_generated_video = generated_video.clone();
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let server = std::thread::spawn(move || {
|
||||
for _ in 0..7 {
|
||||
let (mut stream, _) = listener.accept().expect("accept External video request");
|
||||
for request_index in 0..7 {
|
||||
let mut stream = accept_resource_editor_fixture_connection(
|
||||
&listener,
|
||||
"External video fixture",
|
||||
request_index,
|
||||
);
|
||||
let request = read_http_request(&mut stream);
|
||||
sender
|
||||
.send(request.clone())
|
||||
@@ -5584,9 +5678,6 @@ mod tests {
|
||||
}
|
||||
});
|
||||
let api_key = "resource-editor-external-key";
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
serde_json::json!({"editorApi": {"baseUrl": base_url, "apiKey": api_key}}).to_string(),
|
||||
);
|
||||
let mut request = input(
|
||||
root,
|
||||
Uuid::new_v4().to_string(),
|
||||
@@ -5598,9 +5689,11 @@ mod tests {
|
||||
request.source_media_type = Some(source_asset.media_type.clone());
|
||||
request.asset_name = "源视频编辑版".to_string();
|
||||
|
||||
let result = derive_local_project_resource_at(request.clone())
|
||||
.await
|
||||
.expect("derive External video");
|
||||
let result = with_test_external_editor_credentials(&base_url, api_key, async {
|
||||
derive_local_project_resource_at(request.clone()).await
|
||||
})
|
||||
.await
|
||||
.expect("derive External video");
|
||||
server.join().expect("join External video server");
|
||||
let requests = std::iter::from_fn(|| receiver.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert_eq!(requests.len(), 7);
|
||||
|
||||
@@ -14263,3 +14263,11 @@
|
||||
- 本地 `master` 推送门禁 `scripts/check-repository-ci.sh` 必须在 lint 后运行 Jenkins Web Build 所覆盖的 AGC AppSurface 套件,再执行 build、content 和 diff 检查,避免登录 UI 回归只在远端构建阶段暴露;完整 Vitest 仍由 Jenkins 生产构建执行。
|
||||
- Gitea `repository-checks` 因此必须和 Frontend / Native jobs 一样先执行 AGC 子包的 lockfile 安装;根目录 `npm ci` 不包含 `@tauri-apps/plugin-http` 等子包依赖,不能用预构建镜像缓存假定它们已存在。
|
||||
- 验证:`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts -t "shows a clear login service error|explains a refused login connection|keeps the stored token when startup auth check cannot reach the service"`、`npm run check:git-hooks`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-08-19 AGC 画板恢复测试显式认证模式
|
||||
|
||||
- 背景:普通 Debug 客户端已默认走平台账号路由;旧画板恢复 fixture 只写 `editorApi` 配置,却未安装测试会话或任务级开发者凭据。请求会在到达 loopback mock 前以 `authentication-required` 返回,而 fixture 随后无限等待 `accept`,使 Native shell CI 无界卡住。
|
||||
- 决策:断言 External Editor `external-v1` 路由的测试必须通过 task-local 测试凭据显式进入开发者路径;断言普通客户端恢复路径的测试必须安装可自动恢复的测试平台会话并断言 `/api/runtime/external-generation/jobs/*`。所有等待 mock 请求的 fixture 必须使用有界 accept deadline,不得用无期限 `join` 掩盖请求前失败。
|
||||
- 验证:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml canvas_generation_tests:: -- --test-threads=1`。
|
||||
- 2026-08-19 追加:同一默认认证切换也覆盖资源编辑和 autonomous main-loop fixture。`resource_editor` 的 External Editor 视频提交/轮询/服务身份恢复测试同样使用 task-local 凭据;平台账号语义测试使用隔离测试会话。main-loop 的视觉任务配置测试不再通过旧 `editorApi` 文件伪造登录态。所有 loopback listener 在 accept 时设有 5 秒 deadline,并把 accepted stream 恢复为 blocking,避免 Windows `WouldBlock(10035)` 或请求未发出时无限等待。
|
||||
- 定向验证:`project::asset_canvas::generation::tests::` 14/14、`project::resource_editor::tests::` 36/36、`agent::runtime_driver::main_loop_tests::` 48/48、`agent::runtime_protocol::autonomous_completion_contract_tests::` 107/107 通过。此前一次 Windows 全量 Native Rust 为 1811 passed、108 failed、15 ignored;失败集合仍包含 Provider/mock 调度与既有专业链断言。HEAD 基线独立复现 `tests::project::generate_platform_art_asset_downloads_and_registers_external_image` 的同一登录态缺失,故不能把全量结果伪报为本次 fixture 修复引入;本次新增认证/accept deadline 相关用例均已隔离通过,最后两个 autonomous completion fixture 的认证迁移已单独通过,完整套件未在该两行测试改动后重新执行。
|
||||
|
||||
Reference in New Issue
Block a user