完成资源管理阶段七验收
Project CI / Repository checks (pull_request) Successful in 1m1s
Project CI / Backend tests (pull_request) Successful in 3m43s
Project CI / Native shell tests (pull_request) Successful in 10m47s
Project CI / Frontend tests (pull_request) Failing after 1m41s

补齐视频与资源读取失败空态测试
完成全量前后端和 Rust CI 验收
修复 macOS/BSD 跨平台门禁与测试隔离
更新阶段七验收与共享项目记忆
This commit is contained in:
2026-08-04 10:06:12 +08:00
parent df4e61a208
commit 80bcb4ba0e
26 changed files with 233 additions and 45 deletions
@@ -885,6 +885,10 @@ function readBrowserDom(url) {
function resolveChromeBin() {
for (const candidate of [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
'/opt/google/chrome/chrome',
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/usr/bin/chromium',
@@ -2057,7 +2057,7 @@ mod canvas_generation_tests {
#[tokio::test]
async fn recovery_scan_resumes_accepted_generation_on_default_worker_stack() {
let temporary = tempfile::tempdir().expect("create accepted scan project");
let temporary = crate::tests::canonical_test_tempdir("accepted-generation-scan-");
let root = temporary.path();
init_local_game_project_at(root, "accepted-scan", "恢复扫描测试")
.expect("init accepted scan project");
@@ -736,7 +736,7 @@ mod external_generation_state_tests {
#[test]
fn prepared_generation_state_reuses_identity_and_only_accepted_can_resume() {
let temporary = tempfile::tempdir().expect("create generation ledger project");
let temporary = crate::tests::canonical_test_tempdir("external-generation-ledger-");
let root = temporary.path();
init_local_game_project_at(root, "generation-ledger", "生成账本测试")
.expect("init project");
@@ -846,7 +846,7 @@ mod external_generation_state_tests {
#[test]
fn legacy_completed_generation_persists_only_allowlisted_safe_download_fields() {
let temporary = tempfile::tempdir().expect("create legacy generation ledger project");
let temporary = crate::tests::canonical_test_tempdir("legacy-generation-ledger-");
let root = temporary.path();
init_local_game_project_at(root, "legacy-generation-ledger", "旧同步生成账本测试")
.expect("init project");
@@ -790,7 +790,7 @@ mod tests {
#[test]
fn generation_cleanup_failure_preserves_pending_identity_anchor() {
let temporary = tempfile::tempdir().expect("create pending cleanup project");
let temporary = crate::tests::canonical_test_tempdir("pending-generation-cleanup-");
let root = temporary.path();
let run_id = "generation-cleanup-order-run";
init_local_game_project_at(root, "generation-cleanup-order", "生成账本清理顺序测试")
@@ -425,7 +425,7 @@ mod tests {
const ORDINARY_NOTICE: &str = "除下方有界仓库启动上下文、当前 Session 未压缩对话尾部或历史压缩摘要外,项目记忆、资产和源码正文不会预加载";
const MEMORY_MARKER: &str = "supervisor-preloaded-context-marker";
let directory = tempfile::tempdir().expect("temp project directory");
let directory = crate::tests::canonical_test_tempdir("provider-request-project-");
let root = directory.path().join("project");
init_local_game_project_at(&root, "project-1", "项目总控预加载说明测试")
.expect("project init");
@@ -64,14 +64,8 @@ async fn game_chat_absolute_deadline_returns_an_in_flight_result_before_expiry()
#[tokio::test]
async fn game_chat_absolute_deadline_preserves_external_generation_reconciliation() {
let root = std::env::temp_dir().join(format!(
"genarrative-game-chat-deadline-reconciliation-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock")
.as_nanos()
));
let temporary = crate::tests::canonical_test_tempdir("game-chat-deadline-reconciliation-");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "deadline-reconciliation", "硬截止收尾测试")
.expect("project init");
bind_game_creator_agent_runtime_run_profile_at(
@@ -271,14 +265,8 @@ async fn game_chat_absolute_deadline_preserves_external_generation_reconciliatio
#[test]
fn game_chat_absolute_deadline_still_cleans_local_action_recovery() {
let root = std::env::temp_dir().join(format!(
"genarrative-game-chat-deadline-local-cleanup-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock")
.as_nanos()
));
let temporary = crate::tests::canonical_test_tempdir("game-chat-deadline-local-cleanup-");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "deadline-local-cleanup", "硬截止本地清理测试")
.expect("project init");
let mut runtime = start_game_creator_agent_runtime_task_at(
@@ -767,7 +767,7 @@ fn standard_specialist_empty_plan_has_no_deterministic_final_reply_fallback() {
fn autonomous_manifest_waiting_context_persists_without_finishing_parent_run() {
const RUN_ID: &str = "autonomous-manifest-waiting-parent";
const TASK: &str = "生成完整小游戏并完成项目任务图";
let temporary = tempfile::tempdir().expect("create manifest waiting root");
let temporary = crate::tests::canonical_test_tempdir("manifest-waiting-");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "manifest-waiting-project", TASK)
.expect("init manifest waiting project");
@@ -981,7 +981,7 @@ async fn autonomous_supervisor_converged_final_reply_deserialize_commits_fallbac
const TASK: &str = "生成一个可完成静态检查和双视口试玩的塔防游戏";
const TEST_KEY: &str = "autonomous-final-reply-fallback-key";
let temporary = tempfile::tempdir().expect("create autonomous fallback root");
let temporary = crate::tests::canonical_test_tempdir("autonomous-fallback-");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "autonomous-fallback-project", TASK)
.expect("init autonomous fallback project");
@@ -1221,7 +1221,7 @@ mod orphaned_external_generation_recovery_tests {
#[test]
fn recovery_scan_preserves_active_generation_orphan_then_cleans_terminal_legacy_orphan() {
let temporary = tempfile::tempdir().expect("create orphan generation recovery project");
let temporary = crate::tests::canonical_test_tempdir("orphan-generation-recovery-");
let root = temporary.path();
let run_id = "orphan-generation-recovery-run";
init_local_game_project_at(root, "orphan-generation-recovery", "孤儿生成账本恢复测试")
@@ -827,10 +827,11 @@ mod tests {
.duration_since(UNIX_EPOCH)
.expect("system clock should be after epoch")
.as_nanos();
let root = std::env::temp_dir().join(format!(
"genarrative-context-window-boundary-{}-{unique}",
let temporary = crate::tests::canonical_test_tempdir(&format!(
"context-window-boundary-{}-{unique}-",
std::process::id()
));
let root = temporary.path().join("project");
init_local_game_project_at(&root, "project-1", "上下文窗口边界恢复项目")
.expect("project init");
let mut runtime = start_game_creator_agent_runtime_task_at(
@@ -1931,7 +1931,10 @@ mod tests {
}
fn mcp_test_project(label: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!(
let temp_root = std::env::temp_dir()
.canonicalize()
.expect("canonicalize MCP test temp root");
let root = temp_root.join(format!(
"game-creator-mcp-{label}-{}-{}",
std::process::id(),
MCP_TEST_PROJECT_COUNTER.fetch_add(1, Ordering::Relaxed)
@@ -1606,5 +1606,12 @@ async fn project_supervisor_resume_rechecks_delegate_policy_after_delivery_reser
.expect("read barrier after rejecting reserved delivery")
.is_clear());
let released = wait_for_agent_runtime_lane_release_async(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
)
.await;
assert_eq!(released.state.run_id, parent_run_id);
fs::remove_dir_all(root).ok();
}
@@ -120,6 +120,16 @@ fn unique_project_path() -> PathBuf {
))
}
pub(crate) fn canonical_test_tempdir(prefix: &str) -> tempfile::TempDir {
let temp_root = std::env::temp_dir()
.canonicalize()
.expect("canonicalize test temp root");
tempfile::Builder::new()
.prefix(prefix)
.tempdir_in(temp_root)
.expect("create test temp directory under canonical root")
}
fn agent_goal_sidecar_path_for_test(root: &Path, agent_id: &str, session_id: &str) -> PathBuf {
let path_key = |value: &str| {
format!("{:x}", Sha256::digest(value.as_bytes()))
@@ -56,7 +56,6 @@ import {
type ProjectResource,
type ProjectResourceCategory,
projectResourcesFromReadModels,
type ProjectVersionResourceSummary,
} from './resourceProjectionModel';
import { useProjectResourceCanvasLayout } from './useProjectResourceCanvasLayout';
@@ -1,6 +1,6 @@
import type {
GameIterationVersion,
GameCreationAppManifest,
GameIterationVersion,
ProjectResourceCanvasSection,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
@@ -931,7 +931,7 @@ export function registerProjectWorkbenchFoundationTests() {
expect(restoredCard?.getAttribute('aria-pressed')).toBe('true');
});
it('loads registered documents, art media, and audio inside the central focus state', async () => {
it('loads registered documents, art media, video, and audio with safe failure states inside central focus', async () => {
const manifest = createGameCreationAppManifest(
'workbench-resource-media',
'资源媒体测试',
@@ -951,6 +951,13 @@ export function registerProjectWorkbenchFoundationTests() {
localPath: 'assets/icon.svg',
source: { kind: 'generated' },
},
{
id: 'art-video',
kind: 'animation',
mediaType: 'video/mp4',
localPath: 'assets/intro.mp4',
source: { kind: 'generated' },
},
{
id: 'audio-bgm',
kind: 'bgm',
@@ -958,6 +965,13 @@ export function registerProjectWorkbenchFoundationTests() {
localPath: 'assets/bgm.mp3',
source: { kind: 'generated' },
},
{
id: 'blocked-document',
kind: 'design-document',
mediaType: 'text/markdown',
localPath: 'game/blocked.md',
source: { kind: 'generated' },
},
);
let layoutRevision = 0;
const invoke = vi.fn(
@@ -990,6 +1004,9 @@ export function registerProjectWorkbenchFoundationTests() {
};
}
if (command === 'read_local_project_text_preview') {
if (args?.relativePath === 'game/blocked.md') {
throw new Error('项目权限策略要求用户确认:file.read');
}
expect(args).toMatchObject({
projectPath: '/tmp/workbench-resource-media',
relativePath: 'game/design.md',
@@ -1004,6 +1021,14 @@ export function registerProjectWorkbenchFoundationTests() {
}
if (command === 'read_local_project_media_preview') {
if (args?.category === 'art') {
if (args?.relativePath === 'assets/intro.mp4') {
return {
path: 'assets/intro.mp4',
mediaType: 'video/mp4',
byteLen: 128,
dataUrl: 'data:video/mp4;base64,AAAAIGZ0eXA=',
};
}
return {
path: 'assets/icon.svg',
mediaType: 'image/svg+xml',
@@ -1065,6 +1090,22 @@ export function registerProjectWorkbenchFoundationTests() {
),
);
fireEvent.click(screen.getByRole('button', { name: /intro\.mp4/ }));
const video = (await screen.findByLabelText(
'intro.mp4 视频预览',
)) as HTMLVideoElement;
expect(video.controls).toBe(true);
expect(video.preload).toBe('metadata');
expect(video.getAttribute('src')).toBe(
'data:video/mp4;base64,AAAAIGZ0eXA=',
);
fireEvent.click(
within(screen.getByRole('region', { name: 'intro.mp4' })).getByRole(
'button',
{ name: '收起资源' },
),
);
fireEvent.click(screen.getByRole('button', { name: /bgm\.mp3/ }));
const audio = (await screen.findByLabelText(
'bgm.mp3 音频播放器',
@@ -1076,6 +1117,19 @@ export function registerProjectWorkbenchFoundationTests() {
'read_local_project_media_preview',
expect.objectContaining({ category: 'audio' }),
);
fireEvent.click(
within(screen.getByRole('region', { name: 'bgm.mp3' })).getByRole(
'button',
{ name: '收起资源' },
),
);
fireEvent.click(screen.getByRole('button', { name: /blocked\.md/ }));
expect((await screen.findByRole('alert')).textContent).toBe(
'当前项目策略要求先确认读取资源',
);
expect(screen.getByLabelText('陶泥儿 Agent 对话')).not.toBeNull();
expect(screen.getByLabelText('子 Agent 状态栏')).not.toBeNull();
});
it('renders, filters, highlights, and destroys the resource dependency overlay without pointer previews', async () => {