补齐Gitea测试门禁
Project CI / Backend tests (push) Successful in 5m35s
Project CI / Native shell tests (push) Failing after 11m9s
Project CI / Repository checks (push) Successful in 2m23s
Project CI / Frontend tests (push) Successful in 2m16s

拆分前端、后端与原生壳测试任务
接入server-rs workspace正式全量测试
将AI游戏创作分支及独立依赖纳入CI
修复既有测试断言、格式与异步稳定性问题
稳定AI Tauri并发回执测试
更新开发运维和共享流程文档
This commit is contained in:
2026-07-21 18:12:43 +08:00
parent d8a070f8f8
commit 676c4beb60
27 changed files with 5367 additions and 271 deletions
+308
View File
@@ -0,0 +1,308 @@
name: Project CI
on:
push:
branches:
- master
- codex/ai-game-creator-app
pull_request:
workflow_dispatch:
permissions:
contents: read
env:
CI: 'true'
CARGO_INCREMENTAL: '0'
CARGO_TERM_COLOR: always
RUSTC_WRAPPER: ''
CARGO_BUILD_RUSTC_WRAPPER: ''
jobs:
repository-checks:
name: Repository checks
runs-on: ubuntu-latest
steps:
- name: Checkout full history
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
- name: Install base tools
shell: bash
run: |
set -euo pipefail
command -v apt-get >/dev/null 2>&1 || {
echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2
exit 1
}
sudo_command=''
if command -v sudo >/dev/null 2>&1; then
sudo_command='sudo'
fi
${sudo_command} apt-get update
${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates \
curl
- name: Set up Node.js 22
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Resolve comparison base
shell: bash
run: |
set -euo pipefail
base_ref="$(node -e '
const fs = require("node:fs");
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? "");
')"
if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then
git cat-file -e "${base_ref}^{commit}" 2>/dev/null || {
echo "comparison base commit is unavailable: ${base_ref}" >&2
exit 1
}
else
base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)"
fi
if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \
&& ! git merge-base --is-ancestor "${base_ref}" HEAD; then
echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2
exit 1
fi
echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}"
- name: Set up repository Rust toolchain
shell: bash
run: |
set -euo pipefail
if ! command -v rustup >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain none
fi
echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}"
export PATH="${HOME}/.cargo/bin:${PATH}"
toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)"
test -n "${toolchain}"
rustup toolchain install "${toolchain}" --profile minimal --component rustfmt
rustc --version
cargo --version
rustfmt --version
- name: Install npm dependencies
run: npm ci
- name: Run repository lint gates
run: npm run lint
- name: Build web applications
run: npm run build
- name: Validate content data
run: npm run check:content
- name: Check committed whitespace
shell: bash
run: |
set -euo pipefail
base_ref="${SPACETIME_SCHEMA_BASE_REF:-}"
test -n "${base_ref}"
git cat-file -e "${base_ref}^{commit}"
git diff --check "${base_ref}"...HEAD
frontend-tests:
name: Frontend tests
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: Set up Node.js 22
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Install npm dependencies
run: npm ci
- name: Install AI game creator dependencies
run: npm ci --prefix apps/ai-game-creator-shell
- name: Run frontend and script tests
run: npm run test
backend-tests:
name: Backend tests
runs-on: ubuntu-latest
steps:
- name: Checkout full history
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
- name: Install backend build dependencies
shell: bash
run: |
set -euo pipefail
command -v apt-get >/dev/null 2>&1 || {
echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2
exit 1
}
sudo_command=''
if command -v sudo >/dev/null 2>&1; then
sudo_command='sudo'
fi
${sudo_command} apt-get update
${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
clang \
cmake \
curl \
ffmpeg \
libclang-dev \
libcurl4-openssl-dev \
libssl-dev \
lld \
pkg-config
- name: Set up Node.js 22
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Resolve comparison base
shell: bash
run: |
set -euo pipefail
base_ref="$(node -e '
const fs = require("node:fs");
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? "");
')"
if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then
git cat-file -e "${base_ref}^{commit}" 2>/dev/null || {
echo "comparison base commit is unavailable: ${base_ref}" >&2
exit 1
}
else
base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)"
fi
if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \
&& ! git merge-base --is-ancestor "${base_ref}" HEAD; then
echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2
exit 1
fi
echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}"
- name: Set up repository Rust toolchain
shell: bash
run: |
set -euo pipefail
if ! command -v rustup >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain none
fi
echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}"
export PATH="${HOME}/.cargo/bin:${PATH}"
toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)"
test -n "${toolchain}"
rustup toolchain install "${toolchain}" --profile minimal --component rustfmt
rustc --version
cargo --version
rustfmt --version
- name: Install npm dependencies
run: npm ci
- name: Check server-rs boundaries
run: npm run check:server-rs-ddd
- name: Run server-rs workspace tests
run: cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml
- name: Check api-server targets
run: cargo check --locked -p api-server --all-targets --manifest-path server-rs/Cargo.toml
- name: Check SpacetimeDB module
run: cargo check --locked -p spacetime-module --manifest-path server-rs/Cargo.toml
native-shell-tests:
name: Native shell tests
runs-on: ubuntu-latest
steps:
- name: Checkout full history
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
- name: Install native shell build dependencies
shell: bash
run: |
set -euo pipefail
command -v apt-get >/dev/null 2>&1 || {
echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2
exit 1
}
sudo_command=''
if command -v sudo >/dev/null 2>&1; then
sudo_command='sudo'
fi
${sudo_command} apt-get update
${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
clang \
cmake \
curl \
file \
libayatana-appindicator3-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev \
librsvg2-dev \
lld \
patchelf \
pkg-config \
wget
- name: Set up Node.js 22
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Set up repository Rust toolchain
shell: bash
run: |
set -euo pipefail
if ! command -v rustup >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain none
fi
echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}"
export PATH="${HOME}/.cargo/bin:${PATH}"
toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)"
test -n "${toolchain}"
rustup toolchain install "${toolchain}" --profile minimal --component rustfmt
rustc --version
cargo --version
rustfmt --version
- name: Install npm dependencies
run: npm ci
- name: Install AI game creator dependencies
run: npm ci --prefix apps/ai-game-creator-shell
- name: Run native shell gates
run: npm run check:native-shells
- name: Ensure native lockfiles are unchanged
run: git diff --exit-code -- apps/desktop-shell/src-tauri/Cargo.lock apps/ai-game-creator-shell/src-tauri/Cargo.lock
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
import { spawn } from 'node:child_process';
import { createHash, randomUUID } from 'node:crypto';
import { randomUUID } from 'node:crypto';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
@@ -198,7 +198,9 @@ const server = http.createServer((request, response) => {
let requestJson = null;
try {
requestJson = JSON.parse(requestBody);
} catch {}
} catch {
// Keep the request invalid so the provider fixture can return its normal error path.
}
const content = responses[responseIndex++];
if (!content) {
response.writeHead(500, { 'content-type': 'application/json' });
@@ -885,7 +887,9 @@ function resolveChromeBin() {
try {
accessSync(candidate);
return candidate;
} catch {}
} catch {
// Try the next supported system browser path.
}
}
return 'google-chrome';
}
@@ -6631,6 +6631,18 @@ pub(crate) fn spawn_next_game_creator_agent_background_task_drain(
Ok(())
}
#[cfg(test)]
pub(crate) async fn drain_next_game_creator_agent_background_tasks_for_test(
root: &Path,
agent_id: &str,
) -> Result<(), String> {
let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(root, agent_id)?
.ok_or_else(|| "测试无法获取 Agent Runtime 后台任务锁".to_string())?;
let _runtime_lock = runtime_lock;
drain_next_game_creator_agent_background_tasks(root.to_path_buf(), agent_id.to_string()).await;
Ok(())
}
pub(crate) fn spawn_next_game_creator_agent_background_task_drain_with_lock(
root: &Path,
agent_id: &str,
@@ -24148,24 +24148,15 @@ async fn queued_delegate_receipt_is_suppressed_when_parent_cancels_before_drain(
.expect("cancel parent while receipt is queued");
drop(parent_lock);
spawn_next_game_creator_agent_background_task_drain(&root, "design-director")
drain_next_game_creator_agent_background_tasks_for_test(&root, "design-director")
.await
.expect("drain queued receipt");
let mut drained_receipt = receipt.clone();
// The full suite can saturate Tauri's shared executor with process fixtures.
for _ in 0..1_500 {
let runtime = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read drained receipt runtime");
drained_receipt = runtime
.recent_tasks
.into_iter()
.find(|task| task.run_id == receipt.run_id)
.expect("receipt remains recorded");
if drained_receipt.status != "pending" {
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
let receipt = drained_receipt;
let receipt = read_game_creator_agent_runtime_at(&root, "design-director")
.expect("read drained receipt runtime")
.recent_tasks
.into_iter()
.find(|task| task.run_id == receipt.run_id)
.expect("receipt remains recorded");
assert_eq!(receipt.status, "cancelled");
assert_eq!(receipt.phase, "parent-terminal");
let conversation = read_local_conversation_at(&root, Some("design-director"))
+8 -7
View File
@@ -32,10 +32,6 @@ import {
useRef,
useState,
} from 'react';
import ProjectDevelopmentView, {
type ProjectAgentResultSummary,
type ProjectAgentRuntimeSummary,
} from './view/project-development';
import {
PlatformMudPointWalletEntry,
@@ -105,6 +101,10 @@ import HomeView, {
} from './view/home';
import { useLauncherHomeDraftStore } from './view/home/useHomeDraftStore';
import { type LauncherView, Sidebar } from './view/layout';
import ProjectDevelopmentView, {
type ProjectAgentResultSummary,
type ProjectAgentRuntimeSummary,
} from './view/project-development';
const seedManifest = createGameCreationAppManifest(
'local-project-draft',
@@ -3284,7 +3284,7 @@ function ProjectSupervisorRuntimePanel({
if (runtime?.status === 'failed' || runtime?.phase === 'failed') {
setSupervisorRetryFeedback('');
}
}, [runtime?.runId]);
}, [runtime?.phase, runtime?.runId, runtime?.status]);
const status = projectSupervisorRuntimeStatusLabel(runtime, error);
const collaboratingRuntimes = projectSupervisorCollaboratingAgentRuntimes(
runtime,
@@ -18563,8 +18563,7 @@ export function App({
!projectSupervisorOnly ||
!invoke ||
!nextProjectPath ||
!supervisorRunId ||
!projectSupervisorRuntime
!supervisorRunId
) {
return;
}
@@ -27905,6 +27904,8 @@ export function App({
return () => {
disposed = true;
};
// Candidate contents are fully represented by professionalResultCandidateKey.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
localProject?.projectPath,
professionalResultCandidateKey,
+2 -1
View File
@@ -1,8 +1,9 @@
import './styles.css';
import React from 'react';
import { createRoot } from 'react-dom/client';
import { App, AuthenticatedClient, WorkspaceLauncher } from './App';
import './styles.css';
const initialSearchParams = new URLSearchParams(window.location.search);
const supervisorChatMode =
@@ -16,8 +16,6 @@ import {
Sparkles,
X,
} from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import {
type CSSProperties,
type DragEvent,
@@ -30,6 +28,8 @@ import {
useRef,
useState,
} from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import type {
GameCreationAppAgentGroup,
@@ -1,6 +1,7 @@
/** @vitest-environment jsdom */
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import {
act,
cleanup,
@@ -780,9 +781,7 @@ describe('AI 游戏创作 App 界面边界', () => {
fireEvent.click(riskApproval);
expect(riskApproval.getAttribute('aria-checked')).toBe('false');
expect(riskApproval.getAttribute('data-unavailable')).toBe('true');
expect(
screen.getAllByText('Rank 规则待定,当前暂不可用'),
).toHaveLength(2);
expect(screen.getAllByText('Rank 规则待定,当前暂不可用')).toHaveLength(2);
fireEvent.click(screen.getByRole('button', { name: '完成' }));
expect(
screen.getByRole('button', {
@@ -882,8 +881,12 @@ describe('AI 游戏创作 App 界面边界', () => {
).not.toBeNull();
expect(within(receiptDialog).getByText('仅有美术计划')).not.toBeNull();
expect(within(receiptDialog).getByText('没有图片文件')).not.toBeNull();
expect(within(receiptDialog).getByText('等待实际素材生成。')).not.toBeNull();
expect(receiptDialog.querySelector('.game-resource-focus-body')).not.toBeNull();
expect(
within(receiptDialog).getByText('等待实际素材生成。'),
).not.toBeNull();
expect(
receiptDialog.querySelector('.game-resource-focus-body'),
).not.toBeNull();
expect(receiptDialog.querySelector('ul')).not.toBeNull();
expect(receiptDialog.querySelector('strong')).not.toBeNull();
});
@@ -949,22 +952,24 @@ describe('AI 游戏创作 App 界面边界', () => {
taskId: 'art-asset-plan',
},
});
const invoke = vi.fn(async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_local_project_image_preview') {
expect(args).toEqual({
projectPath: '/tmp/workbench-runnable',
relativePath: 'assets/hero.png',
});
return {
path: 'assets/hero.png',
mediaType: 'image/png',
byteLen: 12,
dataUrl:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
};
}
throw new Error(`unexpected invoke ${command}`);
});
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_local_project_image_preview') {
expect(args).toEqual({
projectPath: '/tmp/workbench-runnable',
relativePath: 'assets/hero.png',
});
return {
path: 'assets/hero.png',
mediaType: 'image/png',
byteLen: 12,
dataUrl:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
render(
@@ -2192,9 +2197,7 @@ describe('AI 游戏创作 App 界面边界', () => {
await waitFor(() => {
expect(rechargeOrderRequestCount).toBe(2);
});
fireEvent.click(
screen.getByRole('button', { name: '关闭购买更多泥点' }),
);
fireEvent.click(screen.getByRole('button', { name: '关闭购买更多泥点' }));
await act(async () => {
releaseLateRechargeOrder?.();
});
@@ -2202,9 +2205,7 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
await screen.findByRole('dialog', { name: '购买更多泥点' }),
).not.toBeNull();
expect(
screen.queryByRole('dialog', { name: '微信扫码支付' }),
).toBeNull();
expect(screen.queryByRole('dialog', { name: '微信扫码支付' })).toBeNull();
});
it('opens the help notice and account menu from the sidebar', () => {
@@ -6958,7 +6959,9 @@ describe('AI 游戏创作 App 界面边界', () => {
agentId: 'project-supervisor',
sessionId: harness.sessionId,
});
expect(within(surface).getByRole('button', { name: '设置' })).not.toBeNull();
expect(
within(surface).getByRole('button', { name: '设置' }),
).not.toBeNull();
expect(screen.queryByLabelText('选择 Agent')).toBeNull();
expect(screen.queryByLabelText('项目总控 Agent 状态')).toBeNull();
expect(screen.queryByLabelText('专业 Agent 协作状态')).toBeNull();
@@ -7007,14 +7010,11 @@ describe('AI 游戏创作 App 界面边界', () => {
);
renderSupervisorChat();
const firstSurface = await screen.findByLabelText(
'项目总控 Agent 纯聊天',
);
const firstSurface = await screen.findByLabelText('项目总控 Agent 纯聊天');
await within(firstSurface).findByLabelText('项目总控消息');
fireEvent.change(
within(firstSurface).getByLabelText('项目总控对话内容'),
{ target: { value: '这条草稿还没有发送' } },
);
fireEvent.change(within(firstSurface).getByLabelText('项目总控对话内容'), {
target: { value: '这条草稿还没有发送' },
});
cleanup();
renderSupervisorChat();
@@ -7131,9 +7131,8 @@ describe('AI 游戏创作 App 界面边界', () => {
);
const surface = await screen.findByLabelText('项目总控 Agent 纯聊天');
let pendingAction = await within(surface).findByLabelText(
'项目总控 Agent 待确认动作',
);
let pendingAction =
await within(surface).findByLabelText('项目总控 Agent 待确认动作');
expect(within(pendingAction).getByText('file.write')).not.toBeNull();
expect(within(pendingAction).getByText('game/index.html')).not.toBeNull();
fireEvent.click(
@@ -7152,9 +7151,8 @@ describe('AI 游戏创作 App 界面边界', () => {
);
});
pendingAction = await within(surface).findByLabelText(
'项目总控 Agent 待确认动作',
);
pendingAction =
await within(surface).findByLabelText('项目总控 Agent 待确认动作');
expect(within(pendingAction).getByText('command.exec')).not.toBeNull();
expect(within(pendingAction).getByText('npm test')).not.toBeNull();
fireEvent.click(
@@ -7216,12 +7214,13 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
within(card).getByText('首版角色规范图采用哪种美术方向?'),
).not.toBeNull();
expect(within(card).getByText('优先验证轮廓与动作可读性。')).not.toBeNull();
expect(
within(card).getByText('优先验证轮廓与动作可读性。'),
).not.toBeNull();
expect(
(within(surface).getByLabelText('项目总控对话内容') as HTMLTextAreaElement)
.disabled,
(
within(surface).getByLabelText(
'项目总控对话内容',
) as HTMLTextAreaElement
).disabled,
).toBe(true);
fireEvent.click(within(card).getByRole('button', { name: /像素风/ }));
@@ -7690,9 +7689,7 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(professionalRuntimeReadCount).toBeGreaterThan(0);
});
expect(screen.queryByLabelText('专业 Agent 实时状态')).toBeNull();
const supervisorStatusPanel = screen.getByLabelText(
'项目总控 Agent 状态',
);
const supervisorStatusPanel = screen.getByLabelText('项目总控 Agent 状态');
supervisorStatusPanel.scrollTop = 120;
const readCountBeforeExpose = professionalRuntimeReadCount;
@@ -7704,9 +7701,7 @@ describe('AI 游戏创作 App 界面边界', () => {
);
expect(professionalRuntimeReadCount).toBeGreaterThan(readCountBeforeExpose);
expect(
within(professionalList).getByText(
'策划 Agent 服务连接失败,请稍后重试',
),
within(professionalList).getByText('策划 Agent 服务连接失败,请稍后重试'),
).not.toBeNull();
expect(professionalList.textContent).not.toContain('fingerprint');
expect(professionalList.textContent).not.toContain('chars=545');
@@ -25219,6 +25214,9 @@ describe('AI 游戏创作 App 界面边界', () => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
@@ -25238,6 +25236,11 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', {
projectPath: '/tmp/authorized-game',
});
});
invoke.mockClear();
submitChat('/policy-confirm file.write');
@@ -25268,6 +25271,9 @@ describe('AI 游戏创作 App 界面边界', () => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
@@ -25300,6 +25306,11 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', {
projectPath: '/tmp/authorized-game',
});
});
invoke.mockClear();
submitChat('/policy-deny not.a.command');
@@ -27902,7 +27913,8 @@ describe('AI 游戏创作 App 界面边界', () => {
await waitFor(() => {
expect(
harness.invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
([command]) =>
command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
@@ -28039,7 +28051,8 @@ describe('AI 游戏创作 App 界面边界', () => {
await waitFor(() => {
expect(
harness.invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
([command]) =>
command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
@@ -28641,7 +28654,8 @@ describe('AI 游戏创作 App 界面边界', () => {
await waitFor(() => {
expect(
harness.invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
([command]) =>
command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
@@ -28862,9 +28876,8 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
screen.getByText('LLM 服务暂时不可用,请检查配置后重试'),
).not.toBeNull();
const professionalList = await screen.findByLabelText(
'专业 Agent 实时状态',
);
const professionalList =
await screen.findByLabelText('专业 Agent 实时状态');
expect(within(professionalList).getByText('程序原型 Agent')).not.toBeNull();
expect(within(professionalList).getByText('分析中')).not.toBeNull();
expect(
@@ -28889,18 +28902,16 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
await screen.findByText('项目总控 Agent 服务连接失败,请稍后重试'),
).not.toBeNull();
expect(screen.queryByText(/private-supervisor-retry-fingerprint/)).toBeNull();
expect(
screen.queryByText(/private-supervisor-retry-fingerprint/),
).toBeNull();
expect(screen.getByText('项目总控 Agent · 失败')).not.toBeNull();
fireEvent.click(
screen.getByRole('button', { name: '在当前项目重试总控' }),
);
fireEvent.click(screen.getByRole('button', { name: '在当前项目重试总控' }));
expect(
screen.getByRole('button', { name: '正在重试项目总控…' }),
).toHaveProperty('disabled', true);
expect(screen.getByRole('status').textContent).toBe(
'正在重试项目总控…',
);
expect(screen.getByRole('status').textContent).toBe('正在重试项目总控…');
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'confirm_retry_game_creator_agent_runtime_task',
@@ -28929,9 +28940,7 @@ describe('AI 游戏创作 App 界面边界', () => {
releaseAcceptedRuntimePoll?.();
expect(
await screen.findByText('项目总控 Agent · 等待确认'),
).not.toBeNull();
expect(await screen.findByText('项目总控 Agent · 等待确认')).not.toBeNull();
expect(screen.getByText('当前阶段:待确认')).not.toBeNull();
expect(screen.queryByText('项目总控 Agent · 失败')).toBeNull();
expect(
@@ -4,7 +4,9 @@ import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import readline from 'node:readline';
import { afterEach, describe, expect, it } from 'vitest';
import { buildProcessSessionFixtureSource } from '../scripts/process-session-real-e2e-fixture.mjs';
const readyPrefix = 'GENARRATIVE_PROCESS_READY';
@@ -63,7 +65,6 @@ describe('process-session real E2E fixture', () => {
const existing = lines.find(predicate);
if (existing) return Promise.resolve(existing);
return new Promise((resolve, reject) => {
let timer: ReturnType<typeof setTimeout>;
const waiter = {
predicate,
resolve: (line) => {
@@ -72,7 +73,7 @@ describe('process-session real E2E fixture', () => {
resolve(line);
},
};
timer = setTimeout(() => {
const timer = setTimeout(() => {
waiters.delete(waiter);
reject(new Error(`process fixture did not emit ${label}`));
}, 3_000);
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest';
import {
createGameCreationAppManifest,
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
type GameCreationAgentRunTrace,
} from '../../../packages/shared/src/contracts/gameCreationApp';
import {
deriveAgentStatusCards,
isAbsoluteProjectPath,
@@ -9,11 +14,6 @@ import {
resolveChatProjectPath,
resolvePendingCommandProjectPath,
} from '../src/App';
import {
createGameCreationAppManifest,
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
type GameCreationAgentRunTrace,
} from '../../../packages/shared/src/contracts/gameCreationApp';
describe('AI 游戏创作聊天记忆命令', () => {
it('recognizes local project absolute paths across desktop platforms', () => {
@@ -450,6 +450,15 @@ DDD 边界检查:
npm run check:server-rs-ddd
```
## Gitea CI 与 PR 检查
- 仓库 CI 入口是 `.gitea/workflows/project-ci.yml`,向 `master``codex/ai-game-creator-app` 推送和所有 PR 创建、更新时必须运行,也允许手工触发。
- CI 固定拆分为 `Repository checks``Frontend tests``Backend tests``Native shell tests` 四个 required job;对应 PR context 完整名称是 `Project CI / Repository checks (pull_request)``Project CI / Frontend tests (pull_request)``Project CI / Backend tests (pull_request)``Project CI / Native shell tests (pull_request)`,首次运行后仍须从 Gitea 最近一周 context 表复核。测试使用独立 job,不能只藏在综合检查 step 中;原生壳验收单独运行以便定位重型构建失败。
- 四个 job 共同覆盖 `npm run check`,并追加 `npm run check:server-rs-ddd``cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml``cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml``cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。后端 runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。`codex/ai-game-creator-app` 分支必须用独立 lockfile 安装 AI 游戏创作壳依赖,其原生壳入口还必须覆盖 `npm run ai-game-creator-shell:check`、release build smoke,并检查 AI Tauri `Cargo.lock` 不漂移。
- checkout 必须使用完整历史。PR 将 base SHA 写入 `SPACETIME_SCHEMA_BASE_REF`,直接推送 `master` 使用 before SHA;事件基线不可解析时直接失败。Gitea 检查的是 PR head 而非预合并 commitworkflow 必须拒绝不包含最新 base commit 的过期 PR,分支保护同时保持“PR 过期禁止合并”。
- 普通 PR job 不读取业务 secret,不运行真实 API/SpacetimeDB/OSS/支付/生成/live smoke,也不执行会修改外部状态的维护、迁移、发布或备份命令。
- Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发和 crates.ioworkflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述四个 job 全部成功。
## 后端相关默认验证
后端修改后,按 DDD 文档中的验收命令执行。涉及 API smoke 时:
@@ -198,6 +198,23 @@ npm run check
`npm run build``scripts/build-gate.mjs` 串行构建主站和后台;该门禁会把 Vite warning 当成失败处理。若看到 `Build gate failed because warnings were emitted`,先看 warning 原文,例如 chunk 体积超过 `vite.config.ts` / `apps/admin-web/vite.config.ts``chunkSizeWarningLimit`,不要先按 Rust 编译失败排查。
### Gitea Actions PR 门禁
仓库级 Gitea Actions 工作流固定为 `.gitea/workflows/project-ci.yml`,在向 `master``codex/ai-game-creator-app` 推送、创建或更新 PR,以及手工触发时运行。工作流拆成四个必须通过的 job:
- `Repository checks`:执行 `npm run lint`、主站与后台生产构建、内容数据检查和提交差异空白检查。
- `Frontend tests`:先按根 lockfile 与 `apps/ai-game-creator-shell/package-lock.json` 分别执行 `npm ci`,再独立执行根 `npm run test`,让 Vitest 文件数和测试数在 Gitea job 列表中明确可见。
- `Backend tests`:执行 `npm run check:server-rs-ddd``cargo test --locked --workspace --no-fail-fast``api-server --all-targets` 编译和 `spacetime-module` 编译;runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。依赖真实服务或密钥的测试必须显式 `ignored`,不能让普通 PR job访问现场环境。
- `Native shell tests`:按根 lockfile 与 AI 游戏创作壳独立 lockfile 安装依赖后执行 `npm run check:native-shells`,覆盖微信壳、Expo 和 Tauri 的完整验收,并确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写,避免把重型原生壳或依赖锁漂移隐藏在基础检查末尾。`codex/ai-game-creator-app` 分支的同名脚本还会执行 `npm run ai-game-creator-shell:check` 和 AI 游戏创作壳 release build smoke。
四个 job 合起来覆盖根 `npm run check`,并补齐根检查没有包含的 server-rs DDD、正式 workspace Rust 测试与现役后端编译门禁。普通 PR CI 不注入业务密钥,不启动真实 API、SpacetimeDB、OSS、支付、图片生成或生产 live smoke;需要现场环境、可变外部状态、Docker 编排或发布凭据的 `check:*` 继续按对应专题和 Jenkins 发布流程执行,不能遍历所有同名前缀脚本冒充 PR 门禁。
PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME_SCHEMA_BASE_REF``check:spacetime-schema` 依赖该基线识别已有表字段删除、改名、重排和改类型;事件给出的基线缺失或本地不可解析时必须直接失败,不能退化为空差异检查。Gitea 的 PR checkout 是 PR head,不是与目标分支的预合并 commit,因此 workflow 还会验证 PR head 包含事件中的最新 base commit;分支保护必须继续开启“PR 过期禁止合并”,过期分支先更新再重跑。向 `master` 直接推送时使用 push before SHA,手工触发时回退到 `origin/master`
启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 `1.26.4`;不得在 `1.26.2` 上执行不受信任 PR 代码。runner 必须提供 `ubuntu-latest` 标签,并将其映射到经验证且固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像;禁止使用浮动镜像 tag,禁止将该标签映射到 host 执行器,禁止向 job 暴露 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 会安装 Node 22、仓库 `rust-toolchain.toml` 固定的 Rust 1.96.0,以及 clang/lld 和 Tauri Linux 依赖;受控 runner 镜像应预装 rustupfallback 下载只用于首次引导。runner 仍需能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm registry、Rust 分发和 crates.io。workflow 中的 `actions/checkout` / `actions/setup-node` 固定到完整 commit;内网 runner 不允许访问 GitHub 时,先把对应 commit 镜像到当前 Gitea 并把 workflow 改为绝对 action URL。首版不使用 Actions cache,避免未配置 runner cache 网络时把缓存恢复错误变成 PR 失败。Gitea 1.26 不执行 workflow 的 `timeout-minutes`,任务最长运行时间在 runner 全局配置收口,不能只在 YAML 写一个不会生效的超时值。
workflow 首次成功运行后,在 Gitea `master` 分支保护中把 `Project CI / Repository checks (pull_request)``Project CI / Frontend tests (pull_request)``Project CI / Backend tests (pull_request)``Project CI / Native shell tests (pull_request)` 四个完整 context 都设为合并必需检查,并从最近一周已上报 context 表复核名称后再保存。不能只填裸 job 名,否则无法匹配 Gitea 实际上报的 `<workflow> / <job> (<event>)`。只提交 workflow 文件不会自动创建 runner,也不会自动修改分支保护;如果 Actions 长时间停留在等待状态,先到仓库或组织的 Actions runner 页面确认存在在线、带 `ubuntu-latest` 标签的 runner。
视觉小说负向扫描与验收门禁:
```bash
-51
View File
@@ -12,8 +12,6 @@
"@lexical/react": "^0.47.0",
"@lexical/utils": "^0.47.0",
"@tailwindcss/vite": "^4.1.14",
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
"@tauri-apps/plugin-opener": "~2",
"@vitejs/plugin-react": "^5.0.4",
"cannon-es": "^0.20.0",
"dotenv": "^17.2.3",
@@ -6986,16 +6984,6 @@
"vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
"node_modules/@tauri-apps/api": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz",
"integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz",
@@ -7213,24 +7201,6 @@
"node": ">= 10"
}
},
"node_modules/@tauri-apps/plugin-clipboard-manager": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.3.2.tgz",
"integrity": "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@tauri-apps/plugin-opener": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz",
"integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.11.0"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
@@ -25650,11 +25620,6 @@
"tailwindcss": "4.2.2"
}
},
"@tauri-apps/api": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz",
"integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="
},
"@tauri-apps/cli": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz",
@@ -25751,22 +25716,6 @@
"dev": true,
"optional": true
},
"@tauri-apps/plugin-clipboard-manager": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.3.2.tgz",
"integrity": "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==",
"requires": {
"@tauri-apps/api": "^2.8.0"
}
},
"@tauri-apps/plugin-opener": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz",
"integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==",
"requires": {
"@tauri-apps/api": "^2.11.0"
}
},
"@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
-2
View File
@@ -164,8 +164,6 @@
"@lexical/react": "^0.47.0",
"@lexical/utils": "^0.47.0",
"@tailwindcss/vite": "^4.1.14",
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
"@tauri-apps/plugin-opener": "~2",
"@vitejs/plugin-react": "^5.0.4",
"cannon-es": "^0.20.0",
"dotenv": "^17.2.3",
@@ -1,18 +1,18 @@
import { describe, expect, it } from 'vitest';
import {
createGameCreationAppManifest,
createGameCreationAppSeedTasks,
GAME_CREATION_AGENT_CAPABILITIES,
GAME_CREATION_AGENT_RUN_MAX_PASSES,
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
GAME_CREATION_AGENT_TOOL_CALL_MAX,
GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
createGameCreationAppSeedTasks,
createGameCreationAppManifest,
GAME_CREATION_APP_COMMANDS,
GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION,
selectGameCreationAppReadyTasks,
type GameCreationAgentRunTrace,
type GameCreationAppManifest,
selectGameCreationAppReadyTasks,
} from './gameCreationApp';
describe('AI 游戏创作 App 共享契约', () => {
@@ -649,8 +649,8 @@ mod tests {
#[tokio::test]
async fn create_story_battle_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, &user_id);
let app = build_router(state);
let response = app
@@ -702,8 +702,8 @@ mod tests {
#[tokio::test]
async fn create_story_npc_battle_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, &user_id);
let app = build_router(state);
let response = app
@@ -804,8 +804,8 @@ mod tests {
#[tokio::test]
async fn get_story_battle_state_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, &user_id);
let app = build_router(state);
let response = app
@@ -841,8 +841,8 @@ mod tests {
#[tokio::test]
async fn resolve_story_battle_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, &user_id);
let app = build_router(state);
let response = app
@@ -946,21 +946,21 @@ mod tests {
.expect("matching runtime session should pass");
}
async fn seed_authenticated_state() -> AppState {
async fn seed_authenticated_state() -> (AppState, String) {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
let user_id = state
.seed_test_phone_user_with_password("13800138107", "secret123")
.await
.id;
state
(state, user_id)
}
fn issue_access_token(state: &AppState) -> String {
fn issue_access_token(state: &AppState, user_id: &str) -> String {
let claims = AccessTokenClaims::from_input(
AccessTokenClaimsInput {
user_id: "user_00000001".to_string(),
user_id: user_id.to_string(),
session_id: state
.seed_test_refresh_session_for_user_id("user_00000001", "sess_story_battles"),
.seed_test_refresh_session_for_user_id(user_id, "sess_story_battles"),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 2,
@@ -666,8 +666,8 @@ mod tests {
#[tokio::test]
async fn begin_story_session_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, &user_id);
let app = build_router(state);
let response = app
@@ -766,8 +766,8 @@ mod tests {
#[tokio::test]
async fn begin_story_runtime_session_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, &user_id);
let app = build_router(state);
let response = app
@@ -841,8 +841,8 @@ mod tests {
#[tokio::test]
async fn resolve_story_runtime_action_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, &user_id);
let app = build_router(state);
let response = app
@@ -888,8 +888,8 @@ mod tests {
#[tokio::test]
async fn continue_story_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, &user_id);
let app = build_router(state);
let response = app
@@ -951,8 +951,8 @@ mod tests {
#[tokio::test]
async fn get_story_session_state_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, &user_id);
let app = build_router(state);
let response = app
@@ -1006,8 +1006,8 @@ mod tests {
#[tokio::test]
async fn get_story_runtime_projection_returns_bad_gateway_when_spacetime_not_published() {
let state = seed_authenticated_state().await;
let token = issue_access_token(&state);
let (state, user_id) = seed_authenticated_state().await;
let token = issue_access_token(&state, &user_id);
let app = build_router(state);
let response = app
@@ -1119,21 +1119,21 @@ mod tests {
.expect("matching actor should pass");
}
async fn seed_authenticated_state() -> AppState {
async fn seed_authenticated_state() -> (AppState, String) {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
let user_id = state
.seed_test_phone_user_with_password("13800138108", "secret123")
.await
.id;
state
(state, user_id)
}
fn issue_access_token(state: &AppState) -> String {
fn issue_access_token(state: &AppState, user_id: &str) -> String {
let claims = AccessTokenClaims::from_input(
AccessTokenClaimsInput {
user_id: "user_00000001".to_string(),
user_id: user_id.to_string(),
session_id: state
.seed_test_refresh_session_for_user_id("user_00000001", "sess_story_sessions"),
.seed_test_refresh_session_for_user_id(user_id, "sess_story_sessions"),
provider: AuthProvider::Password,
roles: vec!["user".to_string()],
token_version: 2,
@@ -884,7 +884,9 @@ pub fn build_game_creation_seed_task_graph(
"Asset",
["art-director"],
["assets/manifest.art.json", "assets/art-spritesheet.png"],
["角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记"],
[
"角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记",
],
),
task(
"art-polish",
@@ -1809,14 +1811,16 @@ mod tests {
let mut wrong_instance = first;
wrong_instance.instance_id = "child-wrong".to_string();
assert!(join_game_creation_isolated_agent_results(
&group,
vec![
wrong_instance,
completed_isolated_result(&group.children[1], "game/b/main.js"),
],
)
.is_err());
assert!(
join_game_creation_isolated_agent_results(
&group,
vec![
wrong_instance,
completed_isolated_result(&group.children[1], "game/b/main.js"),
],
)
.is_err()
);
}
#[test]
@@ -1874,7 +1878,12 @@ mod tests {
art.artifacts,
["assets/manifest.art.json", "assets/art-spritesheet.png"]
);
assert_eq!(art.acceptance_criteria, ["角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记"]);
assert_eq!(
art.acceptance_criteria,
[
"角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记"
]
);
}
#[test]
@@ -1990,9 +1999,10 @@ mod tests {
"publish-package"
]
);
assert!(plan
.carried_task_ids
.contains(&"design-director".to_string()));
assert!(
plan.carried_task_ids
.contains(&"design-director".to_string())
);
assert_eq!(
plan.repair_routes[0].reason,
"code-runtime+dependency-impact"
@@ -2095,14 +2105,16 @@ mod tests {
]
);
assert!(plan.carried_task_ids.contains(&"art-director".to_string()));
assert!(plan
.dependency_waves
.iter()
.any(|wave| wave == &vec!["art-asset-plan".to_string()]));
assert!(plan
.dependency_waves
.iter()
.any(|wave| wave == &vec!["publish-package".to_string()]));
assert!(
plan.dependency_waves
.iter()
.any(|wave| wave == &vec!["art-asset-plan".to_string()])
);
assert!(
plan.dependency_waves
.iter()
.any(|wave| wave == &vec!["publish-package".to_string()])
);
assert_eq!(
plan.repair_routes[0].reason,
"structured-art-asset+dependency-impact"
@@ -198,7 +198,7 @@ fn generated_asset_sheet_muted_green_alpha_requires_explicit_option() {
}
#[test]
fn generated_asset_sheet_magenta_key_preserves_green_white_and_disconnected_key_subject() {
fn generated_asset_sheet_magenta_key_preserves_subject_and_removes_internal_hole() {
let mut sheet = RgbaImage::from_pixel(28, 28, Rgba([255, 0, 255, 255]));
for y in 6..22 {
for x in 6..14 {
@@ -227,8 +227,8 @@ fn generated_asset_sheet_magenta_key_preserves_green_white_and_disconnected_key_
assert_eq!(cleaned.get_pixel(18, 8).0[3], 255);
assert_eq!(
cleaned.get_pixel(13, 13).0[3],
255,
"非边缘连通的 key 色像素不应被当成背景清掉"
0,
"达到阈值的主体内部 key 色镂空区域应被清理"
);
}
@@ -317,7 +317,9 @@ pub fn new_game_creation_app_seed_tasks() -> Vec<GameCreationAppTaskState> {
"Asset",
["art-director"],
["assets/manifest.art.json", "assets/art-spritesheet.png"],
["角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记"],
[
"角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记",
],
),
task(
"art-polish",
@@ -1311,7 +1313,12 @@ mod tests {
art.artifacts,
["assets/manifest.art.json", "assets/art-spritesheet.png"]
);
assert_eq!(art.acceptance_criteria, ["角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记"]);
assert_eq!(
art.acceptance_criteria,
[
"角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记"
]
);
}
#[test]
@@ -1213,7 +1213,7 @@ mod tests {
"themeDescription": " 阳光草坪 ",
"playerImageDescription": " 主角柴犬 ",
"opponentImageDescription": " 对手哈士奇 ",
"onomatopoeia": [" 轰汪! ", "冲啊冲啊冲啊冲啊冲啊!", ""],
"onomatopoeia": [" 轰汪! ", "冲啊冲啊冲啊冲啊冲啊冲啊冲啊", ""],
"playerCharacterImageSrc": "/generated-bark-battle-assets/player.png",
"opponentCharacterImageSrc": "/generated-bark-battle-assets/opponent.png",
"uiBackgroundImageSrc": "/generated-bark-battle-assets/ui.png",
@@ -1232,8 +1232,8 @@ mod tests {
assert!(config_json.contains("/generated-bark-battle-assets/ui.png"));
assert!(config_json.contains("阳光草坪"));
assert!(config_json.contains("轰汪!"));
assert!(config_json.contains("冲啊冲啊冲啊冲啊"));
assert!(!config_json.contains("冲啊冲啊冲啊冲啊冲啊!"));
assert!(config_json.contains("冲啊冲啊冲啊冲啊冲啊冲啊"));
assert!(!config_json.contains("冲啊冲啊冲啊冲啊冲啊冲啊冲啊"));
assert!(!config_json.contains("\"title\":\" 汪汪测试杯 \""));
assert!(!config_json.contains("themePreset"));
assert!(!config_json.contains("playerDogSkinPreset"));
+35 -34
View File
@@ -4220,41 +4220,42 @@ mod tests {
let draft = compile_result_draft(&anchor_pack, &[]);
let candidates = build_generated_candidates("session-1", None, &draft, 2, 1_000_000)
.expect("candidates should build");
let draft = apply_selected_candidate(
PuzzleResultDraft {
levels: vec![module_puzzle::PuzzleDraftLevel {
level_id: "puzzle-level-1".to_string(),
level_name: draft.level_name.clone(),
picture_description: draft
.levels
.first()
.map(|level| level.picture_description.clone())
.unwrap_or_default(),
picture_reference: None,
ui_background_prompt: None,
ui_background_image_src: None,
ui_background_image_object_key: None,
level_scene_image_src: None,
level_scene_image_object_key: None,
ui_spritesheet_image_src: None,
ui_spritesheet_image_object_key: None,
level_background_image_src: None,
level_background_image_object_key: None,
background_music: None,
candidates: candidates.clone(),
selected_candidate_id: None,
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
}],
candidates,
..draft
},
"session-1-candidate-1",
)
.expect("draft should select");
let selected_candidate = candidates.first().expect("candidate should exist");
let draft = normalize_puzzle_draft(PuzzleResultDraft {
levels: vec![module_puzzle::PuzzleDraftLevel {
level_id: "puzzle-level-1".to_string(),
level_name: draft.level_name.clone(),
picture_description: draft
.levels
.first()
.map(|level| level.picture_description.clone())
.unwrap_or_default(),
picture_reference: None,
ui_background_prompt: None,
ui_background_image_src: None,
ui_background_image_object_key: None,
level_scene_image_src: None,
level_scene_image_object_key: Some("generated/puzzle/level-scene.png".to_string()),
ui_spritesheet_image_src: None,
ui_spritesheet_image_object_key: Some(
"generated/puzzle/ui-spritesheet.png".to_string(),
),
level_background_image_src: None,
level_background_image_object_key: Some(
"generated/puzzle/level-background.png".to_string(),
),
background_music: None,
candidates: candidates.clone(),
selected_candidate_id: Some(selected_candidate.candidate_id.clone()),
cover_image_src: Some(selected_candidate.image_src.clone()),
cover_asset_id: Some(selected_candidate.asset_id.clone()),
generation_status: "ready".to_string(),
}],
candidates,
..draft
});
let preview = build_result_preview(&draft, Some("作者"));
assert!(preview.publish_ready);
assert!(preview.publish_ready, "blockers: {:?}", preview.blockers);
}
#[test]
+1 -1
View File
@@ -139,8 +139,8 @@ test('authenticated settings hydrate from remote settings and sync later changes
await waitFor(() => {
expect(storageMocks.getSettings).toHaveBeenCalledTimes(1);
expect(screen.getByTestId('music-volume').textContent).toBe('0.80');
});
expect(screen.getByTestId('music-volume').textContent).toBe('0.80');
vi.useFakeTimers();

Some files were not shown because too many files have changed in this diff Show More