import { describe, expect, it } from 'vitest'; import { buildNormalizationInput, parseOptions, scanScopes, } from './spacetime-normalize-editor-character-actions.mjs'; describe('角色动作元数据规范化脚本', () => { it('默认 dry-run 并要求调用方显式选择 apply', () => { expect( parseOptions(['--database', 'genarrative-prod', '--server', 'prod'], {}), ).toMatchObject({ apply: false, chunkSize: 25, database: 'genarrative-prod', server: 'prod', }); }); it('支持使用环境 token 的 HTTP 调用模式', () => { expect( parseOptions( [ '--database', 'genarrative-dev', '--server-url', 'http://127.0.0.1:3101', '--use-http', ], { GENARRATIVE_SPACETIME_TOKEN: 'dev-migration-token' }, ), ).toMatchObject({ database: 'genarrative-dev', serverUrl: 'http://127.0.0.1:3101', token: 'dev-migration-token', useHttp: true, }); }); it('编码 dry-run cursor 并禁止 canvas 超过五行', () => { expect( buildNormalizationInput({ scope: 'asset', cursor: 'asset-25', limit: 25, dryRun: true, }), ).toEqual({ scope: 'asset', cursor: [0, 'asset-25'], limit: 25, dry_run: true, expected_batch_sha_256: null, }); expect(() => buildNormalizationInput({ scope: 'canvas', limit: 6, dryRun: true, }), ).toThrow('canvas scope limit'); }); it('apply 必须绑定 dry-run 返回的批次哈希', () => { expect(() => buildNormalizationInput({ scope: 'showcase', limit: 25, dryRun: false, }), ).toThrow('batch SHA-256'); expect( buildNormalizationInput({ scope: 'showcase', cursor: null, limit: 25, dryRun: false, expectedBatchSha256: 'a'.repeat(64), }), ).toEqual({ scope: 'showcase', cursor: null, limit: 25, dry_run: false, expected_batch_sha_256: [0, 'a'.repeat(64)], }); }); it('按固定 scope 顺序为每批执行 hash 绑定的 dry-run 与 apply', async () => { const calls: Array> = []; const callProcedure = async ( _options: Record, input: Record, ) => { calls.push(input); const dryRun = input.dry_run === true; const scope = String(input.scope); const hashDigit = { asset: 'a', 'project-resource': 'b', showcase: 'c', canvas: 'd', }[scope]!; const batchSha256 = hashDigit.repeat(64); return { ok: true, scope, dry_run: dryRun, scanned_count: 1, matched_count: 1, updated_count: dryRun ? 0 : 1, blocker_count: 0, blocker_ids: [], next_cursor: null, has_more: false, batch_sha256: batchSha256, error_message: null, }; }; const summaries = await scanScopes( { chunkSize: 25 }, { apply: true, callProcedure }, ); expect(summaries.map((summary) => summary.scope)).toEqual([ 'asset', 'project-resource', 'showcase', 'canvas', ]); expect(calls.map((call) => `${call.scope}:${call.dry_run}`)).toEqual([ 'asset:true', 'asset:false', 'project-resource:true', 'project-resource:false', 'showcase:true', 'showcase:false', 'canvas:true', 'canvas:false', ]); for (let index = 1; index < calls.length; index += 2) { const apply = calls[index]!; const scope = String(apply.scope); const hashDigit = { asset: 'a', 'project-resource': 'b', showcase: 'c', canvas: 'd', }[scope]!; expect(apply.expected_batch_sha_256).toEqual([0, hashDigit.repeat(64)]); } expect(calls.at(-2)?.limit).toBe(5); }); it('拒绝 procedure 返回已访问过的 cursor', async () => { let callCount = 0; const callProcedure = async () => { callCount += 1; const nextCursor = ['asset-25', 'asset-50', 'asset-25'][callCount - 1]; return { ok: true, scope: 'asset', dry_run: true, scanned_count: 1, matched_count: 0, updated_count: 0, blocker_count: 0, blocker_ids: [], next_cursor: nextCursor, has_more: true, batch_sha256: 'a'.repeat(64), error_message: null, }; }; await expect( scanScopes({ chunkSize: 25 }, { callProcedure }), ).rejects.toThrow('重复的 next_cursor: asset-25'); expect(callCount).toBe(3); }); });