修复AGC持续集成断言与空快照竞态
同步原生HTTP权限检查与当前更新器边界。 保持活动回合空快照引用稳定,修正异步测试与窗口发布断言。 补齐图集生成测试的显式切片参数及工具schema断言。 更新技术方案与空快照测试排障记录。
This commit is contained in:
@@ -734,6 +734,9 @@ async fn background_agent_runtime_can_generate_platform_art_asset() {
|
||||
"imageSize": "1K",
|
||||
"assetKind": "art-spritesheet",
|
||||
"assetLabel": "游戏首版核心美术素材",
|
||||
"sliceMode": "grid",
|
||||
"gridX": 2,
|
||||
"gridY": 2,
|
||||
"replaceExisting": false
|
||||
}
|
||||
}
|
||||
@@ -934,6 +937,9 @@ async fn background_agent_runtime_can_generate_platform_art_asset() {
|
||||
request_header(generation_request, "idempotency-key").expect("generation idempotency key");
|
||||
assert!(uuid::Uuid::parse_str(&generation_idempotency_key).is_ok());
|
||||
assert!(generation_request.contains(r#""source":"ai-game-creator-client""#));
|
||||
assert!(generation_request.contains(r#""sliceMode":"grid""#));
|
||||
assert!(generation_request.contains(r#""gridX":2"#));
|
||||
assert!(generation_request.contains(r#""gridY":2"#));
|
||||
assert_eq!(
|
||||
canvas_requests
|
||||
.iter()
|
||||
|
||||
@@ -6916,7 +6916,11 @@ fn agent_native_function_catalog_exposes_each_runtime_tool_with_core_schemas() {
|
||||
"imageSize",
|
||||
"assetKind",
|
||||
"assetLabel",
|
||||
"replaceExisting"
|
||||
"replaceExisting",
|
||||
"sliceMode",
|
||||
"gridX",
|
||||
"gridY",
|
||||
"sliceCount"
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -45,7 +45,7 @@ export function useDirectActiveTurns({
|
||||
* 换掉数组身份:所有依赖 `activeTurns` 的 effect 都会跟着重跑(窗口标题栏的活动项目
|
||||
* 面板就是这么被反复重发布的)。这里只在内容真的变了才更新状态。
|
||||
*/
|
||||
const lastSnapshotSignatureRef = useRef<string>('');
|
||||
const lastSnapshotSignatureRef = useRef<string>('[]');
|
||||
const retryTimerRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -112,7 +112,7 @@ export function useDirectActiveTurns({
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !invoke) {
|
||||
lastSnapshotSignatureRef.current = '';
|
||||
lastSnapshotSignatureRef.current = '[]';
|
||||
// 空态也要保持引用稳定:已经空了就不要再换一个新数组。
|
||||
setActiveTurns((current) => (current.length === 0 ? current : []));
|
||||
setSnapshotReadFailed((current) => (current ? false : current));
|
||||
|
||||
@@ -51,19 +51,23 @@ describe('useDirectActiveTurns', () => {
|
||||
});
|
||||
|
||||
it('clears to a stable empty snapshot when the hook is disabled', async () => {
|
||||
const invoke = vi.fn(
|
||||
async () => [] as GameCreatorDirectActiveTurn[],
|
||||
) as never;
|
||||
let resolveSnapshot!: (turns: GameCreatorDirectActiveTurn[]) => void;
|
||||
const snapshot = new Promise<GameCreatorDirectActiveTurn[]>((resolve) => {
|
||||
resolveSnapshot = resolve;
|
||||
});
|
||||
const invoke = vi.fn(() => snapshot) as never;
|
||||
const { result, rerender } = renderHook(
|
||||
({ enabled }: { enabled: boolean }) =>
|
||||
useDirectActiveTurns({ invoke, enabled, pollIntervalMs: 60_000 }),
|
||||
{ initialProps: { enabled: true } },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeTurns).toEqual([]);
|
||||
});
|
||||
const emptySnapshot = result.current.activeTurns;
|
||||
await act(async () => {
|
||||
resolveSnapshot([]);
|
||||
await snapshot;
|
||||
});
|
||||
expect(result.current.activeTurns).toBe(emptySnapshot);
|
||||
rerender({ enabled: false });
|
||||
expect(result.current.activeTurns).toBe(emptySnapshot);
|
||||
});
|
||||
|
||||
@@ -84,8 +84,8 @@ it('真实窗口与工作台状态同步收敛,回调读取最新处理器且
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
// 无原生 invoke 时 active-turn Hook 会把初始快照归一为空数组一次。
|
||||
expect(publications).toHaveLength(2);
|
||||
// 无原生 invoke 时保持初始空快照,不额外发布相同状态。
|
||||
expect(publications).toHaveLength(1);
|
||||
expect(cleanups).toBe(0);
|
||||
expect(new Set(publications.map((item) => item.onOpenProject)).size).toBe(
|
||||
1,
|
||||
@@ -94,13 +94,13 @@ it('真实窗口与工作台状态同步收敛,回调读取最新处理器且
|
||||
const latestOpen = vi.fn(async () => undefined);
|
||||
homeProjectOverride.openProject = latestOpen;
|
||||
rendered.rerender(view('更改显示名'));
|
||||
expect(publications).toHaveLength(2);
|
||||
expect(publications).toHaveLength(1);
|
||||
act(() => openProject('/tmp/window-latest-project'));
|
||||
expect(latestOpen).toHaveBeenCalledWith(
|
||||
'/tmp/window-latest-project',
|
||||
'open',
|
||||
);
|
||||
expect(publications).toHaveLength(2);
|
||||
expect(publications).toHaveLength(1);
|
||||
expect(cleanups).toBe(0);
|
||||
rendered.unmount();
|
||||
expect(cleanups).toBe(1);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 踩坑与排障记录
|
||||
|
||||
## AGC 空快照测试必须等待请求完成
|
||||
|
||||
`waitFor(() => expect(activeTurns).toEqual([]))` 在 Hook 初始状态就能成功,不能证明首次异步读取已经完成。引用稳定性回归应显式控制 Promise 完成,并同时检查首次空响应与禁用后的引用;快照签名初值必须与初始空数组一致。窗口同步测试应验证未变化状态不重复发布,不能依赖一次多余的空态更新。
|
||||
|
||||
## 2026-09-17 AGC 输入盒的「推理档」弹层被祖先裁切:要放开裁切而不是挪弹层
|
||||
|
||||
- **现象**:窄窗口下(视口 ≤1000px 时右侧对话面板只有 280px 宽)点开输入盒右下角的「推理档」,弹层是个**空盒子**:档位文字(默认 / 低 / 中 / 高 / 最高)整片看不见,只剩一个方框。
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
## 资源画布交互与工作台状态同步
|
||||
|
||||
- 活动回合初始空快照、首次成功读取的空结果及禁用后的空态保持同一数组引用;快照签名初值与空态重置值均为 `[]`。无原生 invoke 的窗口测试只期待首次状态发布,异步空结果测试显式控制请求完成,不以初始空数组作为请求已完成的证据。
|
||||
- 工作台向窗口标题栏发布正在运行的项目时,输入未变化不得形成重复发布与清理的渲染循环;打开项目动作始终使用当前工作台处理逻辑,退出工作台后清除其标题栏状态。
|
||||
- 资源子画布(含「所有资源」)保留空白处左键框选、资源卡左键选中/拖动、触摸板双指平移及捏合缩放;右键按住空白处或资源卡拖动时平移画布,不改变资源选择与布局。中键和空格抓手继续可用。总览保留既有左键平移,并支持右键平移。
|
||||
- 画布接管的右键手势不弹出原生菜单;输入框、媒体操作、工具条和独立浮层不被画布抢占。指针取消、捕获丢失或窗口失焦后终止平移,不能继续跟随指针。
|
||||
|
||||
@@ -2582,7 +2582,6 @@ function assertAiGameCreatorShellUserDevBoundary() {
|
||||
JSON.stringify([
|
||||
{ url: 'https://dev.genarrative.world/api/*' },
|
||||
{ url: 'https://www.genarrative.world/api/*' },
|
||||
{ url: 'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/*' },
|
||||
{ url: 'https://*/api/*' },
|
||||
{ url: 'http://localhost:*/*' },
|
||||
{ url: 'http://127.0.0.1:*/*' },
|
||||
|
||||
Reference in New Issue
Block a user