完成P7验收修复与CI稳定性加固
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Failing after 18s
Project CI / Native shell tests (pull_request) Failing after 7m24s

修正工作台前端 lint 与正式测试切片初始化依赖
补齐可调参数完成态测试夹具并稳定生成恢复 HTTP 读取
修正 macOS Mach-O release 产物识别与原生壳门禁
同步阶段七验收文档与长期排障记忆
This commit is contained in:
2026-08-05 10:00:09 +08:00
parent 3ec3a09380
commit dc7c7dc6cc
10 changed files with 128 additions and 64 deletions
@@ -1487,10 +1487,22 @@ mod canvas_generation_tests {
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.expect("set request read timeout");
let deadline = std::time::Instant::now() + Duration::from_secs(10);
let mut bytes = Vec::new();
let mut buffer = [0_u8; 4096];
loop {
let read = stream.read(&mut buffer).expect("read request bytes");
let read = match stream.read(&mut buffer) {
Ok(read) => read,
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) && std::time::Instant::now() < deadline =>
{
continue;
}
Err(error) => panic!("read request bytes: {error}"),
};
if read == 0 {
break;
}
@@ -152,6 +152,14 @@ fn prepare_autonomous_completion_evidence(root: &Path, state: &AgentRuntimeState
("memory/project.md", "# 项目记忆\n\n正式约束。\n"),
("game/game_design.md", "# 游戏设计\n\n核心循环。\n"),
("game/balance.json", r#"{"lives":3,"speed":1}"#),
(
"game/tunable-parameters.json",
r#"{"schemaVersion":"game-creator-tunable-parameters.v1","parameters":[]}"#,
),
(
"game/tunable-values.json",
r#"{"schemaVersion":"game-creator-tunable-values.v1","values":{}}"#,
),
(
"assets/manifest.art.json",
r#"{"assets":[],"status":"not-generated"}"#,
@@ -1,7 +1,7 @@
import type {
GameHostMessage,
GameHostSlicePayload,
GameHostMessageType,
GameHostSlicePayload,
GameRunSession,
GameRuntimeInspectionMetric,
GameRuntimeMessage,
@@ -41,12 +41,12 @@ import type {
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { resolveTauriInvoke } from '../../app/tauri';
import {
type ActiveGameTestSlice,
beginGameTestSlice,
formalGameTestSlices,
type ActiveGameTestSlice,
type TrustedGameResourceInspection,
projectGameTestSliceMessage,
projectGameTestSliceSessionStatus,
type TrustedGameResourceInspection,
trustedInspectionFromRuntimeMessage,
} from '../../features/project-workspace/gameTestSliceModel';
import {
@@ -580,6 +580,7 @@ export default function ProjectDevelopmentView({
() => formalGameTestSlices(currentRunnableVersion),
[currentRunnableVersion],
);
const firstTestSliceId = testSlices[0]?.sliceId ?? '';
const selectedTestSliceIndex = Math.max(
0,
testSlices.findIndex((slice) => slice.sliceId === selectedTestSliceId),
@@ -808,8 +809,6 @@ export default function ProjectDevelopmentView({
),
[resourcePositionsByCategory],
);
const selectedResource =
resources.find((resource) => resource.id === selectedResourceId) ?? null;
const focusedResource =
resources.find((resource) => resource.id === focusedResourceId) ?? null;
const focusedReplacementBindings =
@@ -950,10 +949,10 @@ export default function ProjectDevelopmentView({
}, [currentRunnableVersion, manifest.projectId, mode, projectPath]);
useEffect(() => {
setSelectedTestSliceId(testSlices[0]?.sliceId ?? '');
setSelectedTestSliceId(firstTestSliceId);
setActiveTestSlice(null);
setTrustedInspection(null);
}, [currentRunnableVersion?.versionId]);
}, [currentRunnableVersion?.versionId, firstTestSliceId]);
useEffect(() => {
setActiveRunnablePreviewVersionId(null);
@@ -733,7 +733,10 @@ for (const snippet of [
'header[3] === 0x46',
'(stat.mode & 0o111) === 0',
'header.readUInt32BE(0)',
'machMagic === 0xcafebabf',
'machMagic === 0xbfbafeca',
'machMagic === 0xfeedfacf',
'machMagic === 0xcffaedfe',
'header[0] !== 0x4d',
'header[1] !== 0x5a',
"console.log('[check:native-shells] desktop-release-binary-artifact')",
@@ -49,9 +49,13 @@ function assertExecutable(filePath, label) {
const machMagic = header.readUInt32BE(0);
const isMachO =
machMagic === 0xcafebabe ||
machMagic === 0xcafed00d ||
machMagic === 0xcafebabf ||
machMagic === 0xbebafeca ||
machMagic === 0xbfbafeca ||
machMagic === 0xfeedface ||
machMagic === 0xfeedfacf;
machMagic === 0xfeedfacf ||
machMagic === 0xcefaedfe ||
machMagic === 0xcffaedfe;
if (!isMachO || (stat.mode & 0o111) === 0) {
throw new Error(`${label} must be an executable Mach-O file`);
}
@@ -564,6 +564,8 @@ type ProjectAgentMudPointAttribution = {
### 7.5 阶段七完整验收
实施状态(2026-08-04):已完成阶段零至阶段六的需求、合同、实现与测试映射复核,并按 `.gitea/workflows/project-ci.yml` 完成仓库门禁、前端、后端 workspace 与原生壳回归。阶段七收口修正了 Rust 格式、前端 import / 未使用变量 lint、正式测试切片初始化 effect 的依赖声明、旧完成态测试夹具缺少可调参数正式产物、生成恢复 mock HTTP 读取对短暂 `WouldBlock` 不稳定,以及 macOS 原生 little-endian Mach-O / FAT64 release 二进制魔数识别;`1280×800` 应用内浏览器验证保持真实登录门禁,`window`、document 与 body 的宽高、滚动尺寸完全一致且控制台无错误。工作台内部结构继续由 AppSurface 集成测试和资源布局 CSS 合同测试验证,不引入认证绕过或伪造 Tauri 环境。
1. 对照飞书需求、当前 PRD、技术方案、代码、测试与阶段提交复核阶段零至阶段六;美术编辑生成新资源继续按本 PRD 已确认的闭环条件暂缓,不作为遗漏或伪完成。
2. AppSurface 同时覆盖文档、图片、SVG、音频和视频聚焦;视频必须使用原生 `controls``preload="metadata"`,读取策略失败时中央主视窗显示安全空态,右侧对话和底部 Agent 状态栏继续存在。
3. `1280×800` 应用内浏览器实测 `window`、document 与 body 均无页面级横向或纵向溢出。浏览器开发页受真实登录门禁保护,不为验收绕过认证或伪造 Tauri;工作台内部结构由 AppSurface 集成测试与资源布局 CSS 合同测试复核。
@@ -4090,3 +4090,19 @@
- 原因:测试触发了与断言无关的账户读取,较快环境中请求会在用例结束前失败,较慢 CI 中请求延迟到组件和 jsdom 均已销毁后才收束。仅用 revision 丢弃旧请求而不在卸载时推进 revision,最后一个在途请求仍会被误认作当前请求。
- 处理:调用方在未认证时不得启动受保护的钱包刷新;可取消的读取要为每轮分配 `AbortController`,新读取先失效并中止旧读取,组件卸载时同时推进 revision、abort 当前请求并清空句柄。所有 `then / catch / finally` 在更新状态前都要检查 signal 与 revision。
- 验证:定向测试覆盖卸载后请求 signal 已中止;同时复跑触发钱包刷新回调的画布生成集成测试和完整前端测试,不能以单文件偶然快速收束代替全量验证。
## macOS release 二进制不能只按大端展示值识别 Mach-O2026-08-04
- 现象:Tauri release build 已成功生成可执行 arm64 二进制,`file` 也识别为 Mach-O,但桌面壳 staging 与 `check:native-shells` 仍报“must be an executable Mach-O file”。
- 原因:原生 Mach-O 文件头通常以 little-endian 字节序写入;用 `readUInt32BE(0)` 读取 arm64 的 `cf fa ed fe` 会得到 `0xcffaedfe`。只接受 `0xfeedfacf` 等大端展示值会误拒绝真实产物,原校验还把 FAT64 魔数误写成了无关值。
- 处理:staging 与根原生壳门禁使用同一完整白名单,同时接受 Mach-O 32/64 与 FAT 32/64 的正向和字节交换魔数;继续校验普通文件、最小体积与可执行权限,不因平台差异放宽产物真实性。
- 验证:macOS 本机执行 `npm run desktop-shell:build -- --no-bundle``npm run desktop-shell:stage-release-binary``npm run check:native-shells`Linux CI 继续验证 ELF,两个 Tauri `Cargo.lock` 在门禁前后必须不变。
- 关联:`apps/desktop-shell/scripts/stage-release-binary.mjs``apps/desktop-shell/scripts/check-config.mjs``scripts/check-native-shells.mjs`
## mock HTTP 已设置读超时仍要容忍短暂 WouldBlock2026-08-04
- 现象:生成恢复定向测试通常通过,但完整原生壳 suite 偶发在读取 loopback 请求时收到 `Resource temporarily unavailable`,导致 mock server 提前退出,主测试随后只看到 channel 断开。
- 原因:macOS 上带读取超时的 `TcpStream::read` 可以用 `WouldBlock` 表示本轮超时;完整 suite 调度压力可能让请求字节晚于单次两秒窗口到达。对第一次 `WouldBlock` 直接 panic 会把调度抖动误判成协议失败。
- 处理:测试 HTTP reader 继续保留单次 socket 超时,并在明确的总 deadline 内重试 `WouldBlock / TimedOut`;超过总 deadline 或出现其它 I/O 错误仍失败关闭。不能移除超时或无限重试,否则真正的请求缺失会把 CI 挂死。
- 验证:定向复跑 `recovery_scan_resumes_accepted_generation_on_default_worker_stack`,并在 `--test-threads=1` 的完整 AI 游戏壳 suite 中确认生成恢复、下载和账本清理全部通过。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs`
+6 -2
View File
@@ -4915,9 +4915,13 @@ function assertDesktopReleaseBinaryArtifact() {
const machMagic = header.readUInt32BE(0);
const isMachO =
machMagic === 0xcafebabe ||
machMagic === 0xcafed00d ||
machMagic === 0xcafebabf ||
machMagic === 0xbebafeca ||
machMagic === 0xbfbafeca ||
machMagic === 0xfeedface ||
machMagic === 0xfeedfacf;
machMagic === 0xfeedfacf ||
machMagic === 0xcefaedfe ||
machMagic === 0xcffaedfe;
if (!isMachO || (stat.mode & 0o111) === 0) {
throw new Error(
'desktop macOS release binary must be an executable Mach-O file',
@@ -2396,9 +2396,11 @@ mod tests {
resource_id: "asset-player-next".to_string(),
},
];
assert!(validate_game_iteration_versions(&[root, invalid_child])
.expect_err("reject duplicate slot")
.contains("资源槽位重复"));
assert!(
validate_game_iteration_versions(&[root, invalid_child])
.expect_err("reject duplicate slot")
.contains("资源槽位重复")
);
}
#[test]
@@ -2552,33 +2554,39 @@ mod tests {
)
.expect("accept valid runnable version graph");
assert!(validate_runnable_game_versions(
"project-runnable",
&[root.clone(), child.clone()],
Some("missing")
)
.expect_err("reject missing current version")
.contains("当前可运行版本不存在"));
assert!(
validate_runnable_game_versions(
"project-runnable",
&[root.clone(), child.clone()],
Some("missing")
)
.expect_err("reject missing current version")
.contains("当前可运行版本不存在")
);
let mut invalid_child = child.clone();
invalid_child.validation.playtest_passed = false;
assert!(validate_runnable_game_versions(
"project-runnable",
&[root.clone(), invalid_child],
Some("runnable-r2")
)
.expect_err("reject incomplete validation")
.contains("缺少完整验证凭证"));
assert!(
validate_runnable_game_versions(
"project-runnable",
&[root.clone(), invalid_child],
Some("runnable-r2")
)
.expect_err("reject incomplete validation")
.contains("缺少完整验证凭证")
);
let mut invalid_child = child;
invalid_child.artifact_path = ".agent/runnable-versions/other/artifact".to_string();
assert!(validate_runnable_game_versions(
"project-runnable",
&[root, invalid_child],
Some("runnable-r2")
)
.expect_err("reject mutable artifact identity")
.contains("不可变产物身份无效"));
assert!(
validate_runnable_game_versions(
"project-runnable",
&[root, invalid_child],
Some("runnable-r2")
)
.expect_err("reject mutable artifact identity")
.contains("不可变产物身份无效")
);
}
#[test]
@@ -2592,45 +2600,53 @@ mod tests {
let mut wrong_version = version.clone();
wrong_version.test_slices[0].version_id = "runnable-r2".to_string();
assert!(validate_runnable_game_versions(
"project-runnable",
&[wrong_version],
Some("runnable-r1")
)
.expect_err("reject a slice owned by another version")
.contains("测试切片版本身份不一致"));
assert!(
validate_runnable_game_versions(
"project-runnable",
&[wrong_version],
Some("runnable-r1")
)
.expect_err("reject a slice owned by another version")
.contains("测试切片版本身份不一致")
);
let mut duplicate_order = version.clone();
let mut second_slice = duplicate_order.test_slices[0].clone();
second_slice.slice_id = "boss".to_string();
duplicate_order.test_slices.push(second_slice);
assert!(validate_runnable_game_versions(
"project-runnable",
&[duplicate_order],
Some("runnable-r1")
)
.expect_err("reject duplicate slice order")
.contains("测试切片 ID 或顺序重复"));
assert!(
validate_runnable_game_versions(
"project-runnable",
&[duplicate_order],
Some("runnable-r1")
)
.expect_err("reject duplicate slice order")
.contains("测试切片 ID 或顺序重复")
);
let mut active_at_registration = version.clone();
active_at_registration.test_slices[0].status = GameTestSliceStatus::Playing;
assert!(validate_runnable_game_versions(
"project-runnable",
&[active_at_registration],
Some("runnable-r1")
)
.expect_err("reject a non-idle registered slice")
.contains("必须以 idle 登记"));
assert!(
validate_runnable_game_versions(
"project-runnable",
&[active_at_registration],
Some("runnable-r1")
)
.expect_err("reject a non-idle registered slice")
.contains("必须以 idle 登记")
);
let mut unbound_descriptor = version;
unbound_descriptor.resource_descriptors[0].resource_id = "unbound".to_string();
assert!(validate_runnable_game_versions(
"project-runnable",
&[unbound_descriptor],
Some("runnable-r1")
)
.expect_err("reject an unbound trusted descriptor")
.contains("资源描述未绑定"));
assert!(
validate_runnable_game_versions(
"project-runnable",
&[unbound_descriptor],
Some("runnable-r1")
)
.expect_err("reject an unbound trusted descriptor")
.contains("资源描述未绑定")
);
}
#[test]