补齐Gitea测试门禁
拆分前端、后端与原生壳测试任务 接入server-rs workspace正式全量测试 将AI游戏创作分支及独立依赖纳入CI 修复既有测试断言、格式与异步稳定性问题 稳定AI Tauri并发回执测试 更新开发运维和共享流程文档
This commit is contained in:
@@ -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
|
||||
+4771
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"))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 而非预合并 commit,workflow 必须拒绝不包含最新 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.io;workflow 的官方 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 镜像应预装 rustup,fallback 下载只用于首次引导。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
|
||||
|
||||
Generated
-51
@@ -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",
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user