fd0c1007ad
删除 63 张旧玩法表的 module 定义与 legacy schema 移除清空 procedure、迁移白名单和旧行兼容逻辑 重新生成 spacetime-client bindings 新增一次性 schema guard 删除白名单与回归测试 清理后台表名映射、旧回填工具和权威文档
197 lines
7.0 KiB
TypeScript
197 lines
7.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
APPROVED_RETIRED_TABLE_DELETIONS,
|
|
collectTablesFromSources,
|
|
compareTables,
|
|
isFormalGateEnvironment,
|
|
listReachableRustFiles,
|
|
resolveBaseRef,
|
|
} from './check-spacetime-schema-guard.mjs';
|
|
|
|
const manifestPath = 'server-rs/crates/spacetime-module/Cargo.toml';
|
|
const sourceRoot = 'server-rs/crates/spacetime-module/src';
|
|
|
|
type SourceFiles = Record<string, string>;
|
|
|
|
function createSourceReader(files: SourceFiles) {
|
|
const sources = new Map(Object.entries(files));
|
|
return (path: string) => sources.get(path) ?? null;
|
|
}
|
|
|
|
function collectReachableTables(files: SourceFiles) {
|
|
const readSource = createSourceReader(files);
|
|
const paths = listReachableRustFiles(readSource);
|
|
return {
|
|
paths,
|
|
result: collectTablesFromSources(
|
|
paths.map((path: string) => ({ path, text: readSource(path) ?? '' })),
|
|
),
|
|
};
|
|
}
|
|
|
|
function tableSource(structName: string, accessor: string) {
|
|
return `#[spacetimedb::table(accessor = ${accessor})]\npub struct ${structName} {\n pub id: u64,\n}\n`;
|
|
}
|
|
|
|
function createGitResolver(entries: Record<string, string | null>) {
|
|
return (args: string[]) => entries[args.join(' ')] ?? null;
|
|
}
|
|
|
|
function collectTables(accessors: string[]) {
|
|
return collectTablesFromSources(
|
|
accessors.map((accessor, index) => ({
|
|
path: `${sourceRoot}/schema.rs`,
|
|
text: tableSource(`Table${index}`, accessor),
|
|
})),
|
|
).tables;
|
|
}
|
|
|
|
describe('SpacetimeDB schema guard base resolution', () => {
|
|
it('prefers an explicit Jenkins-provided base ref', () => {
|
|
expect(
|
|
resolveBaseRef({
|
|
argv: ['node', 'check-spacetime-schema-guard.mjs'],
|
|
env: {
|
|
CI: 'true',
|
|
SPACETIME_SCHEMA_BASE_REF: 'parent-commit',
|
|
},
|
|
git: createGitResolver({}),
|
|
}),
|
|
).toBe('parent-commit');
|
|
});
|
|
|
|
it('uses HEAD parent when the remote master ref resolves to HEAD', () => {
|
|
expect(
|
|
resolveBaseRef({
|
|
argv: ['node', 'check-spacetime-schema-guard.mjs'],
|
|
env: { CI: 'true' },
|
|
git: createGitResolver({
|
|
'rev-parse --verify HEAD^{commit}': 'current-commit',
|
|
'merge-base HEAD origin/master': 'current-commit',
|
|
'rev-parse --verify origin/master^{commit}': 'current-commit',
|
|
'rev-parse --verify HEAD^': 'parent-commit',
|
|
}),
|
|
}),
|
|
).toBe('parent-commit');
|
|
});
|
|
|
|
it('fails closed in formal gates when no distinct commit is available', () => {
|
|
expect(() =>
|
|
resolveBaseRef({
|
|
argv: ['node', 'check-spacetime-schema-guard.mjs'],
|
|
env: { JENKINS_URL: 'https://jenkins.example.test/' },
|
|
git: createGitResolver({
|
|
'rev-parse --verify HEAD^{commit}': 'current-commit',
|
|
'merge-base HEAD origin/master': 'current-commit',
|
|
'rev-parse --verify origin/master^{commit}': 'current-commit',
|
|
}),
|
|
}),
|
|
).toThrow(/无法取得与 HEAD 不同/u);
|
|
});
|
|
|
|
it('keeps HEAD fallback for a local initial repository', () => {
|
|
expect(
|
|
resolveBaseRef({
|
|
argv: ['node', 'check-spacetime-schema-guard.mjs'],
|
|
env: {},
|
|
git: createGitResolver({
|
|
'rev-parse --verify HEAD^{commit}': 'initial-commit',
|
|
}),
|
|
}),
|
|
).toBe('HEAD');
|
|
expect(isFormalGateEnvironment({ CI: 'false' })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('SpacetimeDB schema guard module reachability', () => {
|
|
it('ignores duplicate accessors in retained but unreachable legacy sources', () => {
|
|
const activePath = `${sourceRoot}/active.rs`;
|
|
const shellPath = `${sourceRoot}/legacy_schema/example.rs`;
|
|
const retiredPath = `${sourceRoot}/example.rs`;
|
|
const { paths, result } = collectReachableTables({
|
|
[manifestPath]: '[lib]\npath = "src/active.rs"\n',
|
|
[activePath]: '#[path = "legacy_schema/example.rs"]\nmod example;\n',
|
|
[shellPath]: tableSource('ExampleSchemaShell', 'example'),
|
|
[retiredPath]: tableSource('RetiredExample', 'example'),
|
|
});
|
|
|
|
expect(paths).toEqual([activePath, shellPath]);
|
|
expect(result.failures).toEqual([]);
|
|
expect([...result.tables.keys()]).toEqual(['example']);
|
|
});
|
|
|
|
it('still rejects duplicate accessors when both definitions are reachable', () => {
|
|
const activePath = `${sourceRoot}/active.rs`;
|
|
const firstPath = `${sourceRoot}/first.rs`;
|
|
const secondPath = `${sourceRoot}/second.rs`;
|
|
const { result } = collectReachableTables({
|
|
[manifestPath]: '[lib]\npath = "src/active.rs"\n',
|
|
[activePath]: 'mod first;\nmod second;\n',
|
|
[firstPath]: tableSource('FirstExample', 'example'),
|
|
[secondPath]: tableSource('SecondExample', 'example'),
|
|
});
|
|
|
|
expect(result.failures).toHaveLength(1);
|
|
expect(result.failures[0]).toMatch(/table accessor example 重复定义/u);
|
|
});
|
|
});
|
|
|
|
describe('SpacetimeDB schema guard retired table deletion allowlist', () => {
|
|
it('allows exactly the 63 approved historical tables to disappear', () => {
|
|
expect(APPROVED_RETIRED_TABLE_DELETIONS).toHaveLength(63);
|
|
expect(new Set(APPROVED_RETIRED_TABLE_DELETIONS).size).toBe(63);
|
|
|
|
const result = compareTables(
|
|
collectTables([...APPROVED_RETIRED_TABLE_DELETIONS]),
|
|
new Map(),
|
|
);
|
|
|
|
expect(result.failures).toEqual([]);
|
|
expect(result.schemaChanged).toBe(true);
|
|
expect(result.breakingChanged).toBe(false);
|
|
});
|
|
|
|
it('still rejects deletion of a table outside the approved list', () => {
|
|
const result = compareTables(
|
|
collectTables([APPROVED_RETIRED_TABLE_DELETIONS[0], 'active_table']),
|
|
new Map(),
|
|
);
|
|
|
|
expect(result.failures).toHaveLength(1);
|
|
expect(result.failures[0]).toMatch(/active_table.*被删除或改名/u);
|
|
expect(result.schemaChanged).toBe(true);
|
|
expect(result.breakingChanged).toBe(true);
|
|
});
|
|
|
|
it('does not suppress retained-table field deletion or rename checks', () => {
|
|
const baseTables = collectTablesFromSources([
|
|
{
|
|
path: `${sourceRoot}/ranks.rs`,
|
|
text: `#[spacetimedb::table(accessor = retained_deleted_field)]\npub struct RetainedDeletedField {\n pub id: u64,\n pub title: String,\n}\n`,
|
|
},
|
|
{
|
|
path: `${sourceRoot}/titles.rs`,
|
|
text: `#[spacetimedb::table(accessor = retained_renamed_field)]\npub struct RetainedRenamedField {\n pub id: u64,\n pub title: String,\n}\n`,
|
|
},
|
|
]).tables;
|
|
const currentTables = collectTablesFromSources([
|
|
{
|
|
path: `${sourceRoot}/ranks.rs`,
|
|
text: `#[spacetimedb::table(accessor = retained_deleted_field)]\npub struct RetainedDeletedField {\n pub id: u64,\n}\n`,
|
|
},
|
|
{
|
|
path: `${sourceRoot}/titles.rs`,
|
|
text: `#[spacetimedb::table(accessor = retained_renamed_field)]\npub struct RetainedRenamedField {\n pub id: u64,\n pub name: String,\n}\n`,
|
|
},
|
|
]).tables;
|
|
|
|
const result = compareTables(baseTables, currentTables);
|
|
|
|
expect(result.failures).toHaveLength(2);
|
|
expect(result.failures.join('\n')).toMatch(/字段数量减少/u);
|
|
expect(result.failures.join('\n')).toMatch(/字段被删除或改名/u);
|
|
expect(result.breakingChanged).toBe(true);
|
|
});
|
|
});
|