diff --git a/.gitea/workflows/project-ci.yml b/.gitea/workflows/project-ci.yml index 7bdf6a98a..1602c9325 100644 --- a/.gitea/workflows/project-ci.yml +++ b/.gitea/workflows/project-ci.yml @@ -185,6 +185,9 @@ jobs: - name: Run server-rs workspace tests run: cargo test --locked --workspace --exclude spacetime-module --no-fail-fast --manifest-path server-rs/Cargo.toml + - name: Run SpacetimeDB module unit tests + run: cargo test --locked -p spacetime-module --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 diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index eaf23e66c..44388ee00 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -62,4 +62,4 @@ SpacetimeDB 任务统一先读取 `.codex/skills/genarrative-spacetimedb/SKILL.m ## Gitea CI 依赖闭合 -`.gitea/workflows/project-ci.yml` 的 `Native shell tests` 在运行原生壳门禁前,必须使用 `cargo fetch --locked` 预取 `server-rs/Cargo.toml`、桌面壳和 AGC 壳三份依赖。Backend workspace host tests 暂时使用 `cargo test --locked --workspace --exclude spacetime-module --no-fail-fast`:`spacetime-module` 是依赖 SpacetimeDB WASM 宿主导出的 `cdylib`,不能链接成普通 host test executable;Backend 仍通过独立的 `cargo check --locked -p spacetime-module` 验证模块源码。该排除只是 CI 适配,不代表模块测试永久取消;恢复前需要提供 SpacetimeDB runtime/integration harness,或把纯 host 可测逻辑拆到独立 crate,并补回对应测试门禁。AGC 壳检查还会运行 `platform-llm` 与 `shared-contracts` 的 server-rs workspace 测试,这些命令以及 AGC 壳测试必须带 `--locked`,避免在测试阶段重新解析 registry index;锁文件发生变化时应先更新受信任 CI 镜像缓存,再重跑门禁。 +`.gitea/workflows/project-ci.yml` 的 `Native shell tests` 在运行原生壳门禁前,必须使用 `cargo fetch --locked` 预取 `server-rs/Cargo.toml`、桌面壳和 AGC 壳三份依赖。Backend host workspace tests 使用 `cargo test --locked --workspace --exclude spacetime-module --no-fail-fast`,避免 `spacetime-module` 的 `spacetime-types` feature 统一污染普通领域 crate 的 host 测试;随后单独执行 `cargo test --locked -p spacetime-module --no-fail-fast`,由 `spacetime-module/src/active.rs` 在 host 测试构建期间提供仅测试期的 SpacetimeDB ABI 链接支持,使该 crate 的纯单元测试也纳入 Backend 门禁。`spacetime-module` 的 reducer / procedure 运行时行为仍必须通过真实 SpacetimeDB runtime/integration harness 验证,host 链接支持不得被当作运行时替身。Backend 另外执行 `cargo check --locked -p spacetime-module` 验证模块源码。AGC 壳检查还会运行 `platform-llm` 与 `shared-contracts` 的 server-rs workspace 测试,这些命令以及 AGC 壳测试必须带 `--locked`,避免在测试阶段重新解析 registry index;锁文件发生变化时应先更新受信任 CI 镜像缓存,再重跑门禁。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 3a4b38e42..9e2ff473d 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -264,7 +264,7 @@ npm run check - `Repository checks`:调用唯一入口 `npm run check:repository-ci`,执行 `npm run lint`、AI 游戏创作壳 AppSurface 定向测试、主站与后台生产构建和提交差异空白检查。本地 master `pre-push` 复用同一入口,禁止在 workflow 与 hook 中维护两份近似命令。 - `Frontend tests`:按唯一根 workspace lockfile 执行一次干净的 `npm ci`,再独立执行根 `npm run test`、`npm run bgfilter-worker:smoke-test`、`npm run check:production-health-patrol`、`npm run check:production-api-release` 和 `npm run check:production-api-deploy`,让 Vitest、Node test smoke harness 及不依赖真实服务的生产巡检 / 发布 / 部署行为 fixture 在 Gitea job 中持续执行;其中 `.test.mjs` 使用 Node test runner,不依赖 Vitest 的 `scripts/**/*.test.ts` 收集规则。 -- `Backend tests`:先对 `server-rs/Cargo.lock` 执行带 5 次整命令级有界重试的 `cargo fetch --locked`,再执行 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --exclude spacetime-module --no-fail-fast`、`api-server --all-targets` 编译和 `cargo check --locked -p spacetime-module`;`spacetime-module` 是依赖 SpacetimeDB WASM 宿主导出的 `cdylib`,不能链接成普通 host test executable,因此暂时不进入 host workspace test。该排除不是永久取消模块验证;恢复前需要 SpacetimeDB runtime/integration harness,或把纯 host 可测逻辑拆到独立 crate,并补回对应测试门禁。依赖准备必须位于会触发 Cargo build 的 DDD / 产物边界门禁之前,避免锁新增依赖未命中镜像缓存时绕过既有下载重试。runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。依赖真实服务或密钥的测试必须显式 `ignored`,不能让普通 PR job访问现场环境。 +- `Backend tests`:先对 `server-rs/Cargo.lock` 执行带 5 次整命令级有界重试的 `cargo fetch --locked`,再执行 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --exclude spacetime-module --no-fail-fast`、`cargo test --locked -p spacetime-module --no-fail-fast`、`api-server --all-targets` 编译和 `cargo check --locked -p spacetime-module`;普通 workspace host 测试排除 `spacetime-module` 以避免其 `spacetime-types` feature 统一污染领域 crate,模块自身的纯单元测试通过独立 package test 纳入门禁。`spacetime-module` 的 reducer / procedure 运行时行为仍必须通过真实 SpacetimeDB runtime/integration harness 验证,不能把 host 链接支持当作运行时替身。依赖准备必须位于会触发 Cargo build 的 DDD / 产物边界门禁之前,避免锁新增依赖未命中镜像缓存时绕过既有下载重试。runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。依赖真实服务或密钥的测试必须显式 `ignored`,不能让普通 PR job访问现场环境。 - `Native shell tests`:按唯一根 workspace lockfile 安装全部 App 依赖后执行 `npm run check:native-shells`,对所有触发方式一致覆盖微信壳、Expo 和 Tauri 的完整验收,并执行 `npm run ai-game-creator-shell:check` 与 AI 游戏创作壳 release build smoke;最后确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写。共享 Agent Runtime 后台锁 suite 固定 `--test-threads=1`,不能用并行偶发失败后的逐项通过替代整套稳定门禁。 四个 job 合起来覆盖根 `npm run check`,并补齐根检查没有包含的 BgFilter worker smoke harness、无密钥生产巡检 / 发布 / 部署行为 fixture、server-rs DDD、正式 workspace Rust 测试与现役后端编译门禁。普通 PR CI 不注入业务密钥,不启动真实 API、SpacetimeDB、OSS、支付、图片生成或生产 live smoke;需要现场环境、可变外部状态、Docker 编排或发布凭据的 `check:*` 继续按对应专题和 Jenkins 发布流程执行,不能遍历所有同名前缀脚本冒充 PR 门禁。 diff --git a/scripts/project-ci-workflow.test.ts b/scripts/project-ci-workflow.test.ts index 30c553a45..0e1fd681b 100644 --- a/scripts/project-ci-workflow.test.ts +++ b/scripts/project-ci-workflow.test.ts @@ -284,6 +284,9 @@ describe('project CI workflow', () => { expect(workflow).toContain( 'cargo test --locked --workspace --exclude spacetime-module --no-fail-fast --manifest-path server-rs/Cargo.toml', ); + expect(workflow).toContain( + 'cargo test --locked -p spacetime-module --no-fail-fast --manifest-path server-rs/Cargo.toml', + ); }); it('never passes HEAD itself to the schema comparison gate', () => { diff --git a/server-rs/crates/spacetime-module/src/active.rs b/server-rs/crates/spacetime-module/src/active.rs index b05114f2f..415a2c742 100644 --- a/server-rs/crates/spacetime-module/src/active.rs +++ b/server-rs/crates/spacetime-module/src/active.rs @@ -65,3 +65,199 @@ pub use runtime::*; pub use square_hole::*; pub use visual_novel::*; pub use wooden_fish::*; + +// Host-side unit tests need to link the module crate as a normal test binary. +// SpacetimeDB's raw ABI imports only exist in the WASM host, so provide +// deterministic error-returning symbols for tests that exercise pure helpers. +// Reducer/procedure integration tests must use a real SpacetimeDB runtime. +#[cfg(all(test, not(target_arch = "wasm32")))] +mod host_test_imports { + type TableId = u32; + type IndexId = u32; + type ColId = u16; + type BytesSource = u32; + type BytesSink = u32; + type RowIter = u32; + + const HOST_TEST_UNSUPPORTED: u16 = 1; + + #[unsafe(no_mangle)] + pub extern "C" fn table_id_from_name(_: *const u8, _: usize, _: *mut TableId) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn index_id_from_name(_: *const u8, _: usize, _: *mut IndexId) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn datastore_table_row_count(_: TableId, _: *mut u64) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn datastore_table_scan_bsatn(_: TableId, _: *mut RowIter) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn datastore_index_scan_range_bsatn( + _: IndexId, + _: *const u8, + _: usize, + _: ColId, + _: *const u8, + _: usize, + _: *const u8, + _: usize, + _: *mut RowIter, + ) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn datastore_delete_by_index_scan_range_bsatn( + _: IndexId, + _: *const u8, + _: usize, + _: ColId, + _: *const u8, + _: usize, + _: *const u8, + _: usize, + _: *mut u32, + ) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn datastore_delete_all_by_eq_bsatn( + _: TableId, + _: *const u8, + _: usize, + _: *mut u32, + ) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn row_iter_bsatn_advance(_: RowIter, _: *mut u8, _: *mut usize) -> i16 { + -1 + } + + #[unsafe(no_mangle)] + pub extern "C" fn row_iter_bsatn_close(_: RowIter) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn datastore_insert_bsatn(_: TableId, _: *mut u8, _: *mut usize) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn datastore_update_bsatn( + _: TableId, + _: IndexId, + _: *mut u8, + _: *mut usize, + ) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn bytes_sink_write(_: BytesSink, _: *const u8, _: *mut usize) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn bytes_source_read(_: BytesSource, _: *mut u8, _: *mut usize) -> i16 { + -1 + } + + #[unsafe(no_mangle)] + pub extern "C" fn console_log( + _: u8, + _: *const u8, + _: usize, + _: *const u8, + _: usize, + _: u32, + _: *const u8, + _: usize, + ) { + } + + #[unsafe(no_mangle)] + pub extern "C" fn console_timer_start(_: *const u8, _: usize) -> u32 { + 0 + } + + #[unsafe(no_mangle)] + pub extern "C" fn console_timer_end(_: u32) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn identity(out: *mut u8) { + if !out.is_null() { + unsafe { std::ptr::write_bytes(out, 0, 32) }; + } + } + + #[unsafe(no_mangle)] + pub extern "C" fn bytes_source_remaining_length(_: BytesSource, _: *mut u32) -> i16 { + HOST_TEST_UNSUPPORTED as i16 + } + + #[unsafe(no_mangle)] + pub extern "C" fn get_jwt(_: *const u8, _: *mut BytesSource) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn procedure_sleep_until(wake_at: i64) -> i64 { + wake_at + } + + #[unsafe(no_mangle)] + pub extern "C" fn procedure_start_mut_tx(_: *mut i64) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn procedure_commit_mut_tx() -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn procedure_abort_mut_tx() -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn datastore_index_scan_point_bsatn( + _: IndexId, + _: *const u8, + _: usize, + _: *mut RowIter, + ) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn datastore_delete_by_index_scan_point_bsatn( + _: IndexId, + _: *const u8, + _: usize, + _: *mut u32, + ) -> u16 { + HOST_TEST_UNSUPPORTED + } + + #[unsafe(no_mangle)] + pub extern "C" fn datastore_clear(_: TableId, _: *mut u64) -> u16 { + HOST_TEST_UNSUPPORTED + } +}