3d8e0211db
Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/159 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
198 lines
5.3 KiB
TypeScript
198 lines
5.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
buildCleanupInput,
|
|
parseOptions,
|
|
scanScopes,
|
|
} from './spacetime-clean-editor-image-asset-kind.mjs';
|
|
|
|
describe('普通图片 assetKind 清理脚本', () => {
|
|
it('默认 dry-run 并要求调用方显式选择 apply', () => {
|
|
expect(
|
|
parseOptions(['--database', 'genarrative-prod', '--server', 'prod'], {}),
|
|
).toMatchObject({
|
|
apply: false,
|
|
chunkSize: 25,
|
|
database: 'genarrative-prod',
|
|
server: 'prod',
|
|
});
|
|
});
|
|
|
|
it('编码 cursor、限制 canvas 批量并要求 apply hash', () => {
|
|
expect(
|
|
buildCleanupInput({
|
|
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(() =>
|
|
buildCleanupInput({ scope: 'canvas', limit: 6, dryRun: true }),
|
|
).toThrow('canvas scope limit');
|
|
expect(() =>
|
|
buildCleanupInput({ scope: 'showcase', limit: 25, dryRun: false }),
|
|
).toThrow('batch SHA-256');
|
|
});
|
|
|
|
it('按固定 scope 顺序执行 hash 绑定 apply 并保留字段计数', async () => {
|
|
const calls: Array<Record<string, unknown>> = [];
|
|
const callProcedure = async (
|
|
_options: Record<string, unknown>,
|
|
input: Record<string, unknown>,
|
|
) => {
|
|
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]!;
|
|
return {
|
|
ok: true,
|
|
scope,
|
|
dry_run: dryRun,
|
|
scanned_count: 1,
|
|
matched_count: 1,
|
|
updated_count: dryRun ? 0 : 1,
|
|
cleaned_field_count: scope === 'canvas' ? 3 : 1,
|
|
blocker_count: 0,
|
|
blocker_samples: [],
|
|
next_cursor: null,
|
|
has_more: false,
|
|
batch_sha256: hashDigit.repeat(64),
|
|
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',
|
|
]);
|
|
expect(summaries.at(-1)?.cleaned_field_count).toBe(3);
|
|
expect(calls.at(-2)?.limit).toBe(5);
|
|
for (let index = 1; index < calls.length; index += 2) {
|
|
expect(calls[index]?.expected_batch_sha_256).toEqual([
|
|
0,
|
|
String(
|
|
calls[index - 1]?.scope === 'asset'
|
|
? 'a'
|
|
: calls[index - 1]?.scope === 'project-resource'
|
|
? 'b'
|
|
: calls[index - 1]?.scope === 'showcase'
|
|
? 'c'
|
|
: 'd',
|
|
).repeat(64),
|
|
]);
|
|
}
|
|
});
|
|
|
|
it('拒绝 apply 后计数漂移与复核残留', async () => {
|
|
const driftingCall = async (
|
|
_options: Record<string, unknown>,
|
|
input: Record<string, unknown>,
|
|
) => ({
|
|
ok: true,
|
|
scope: input.scope,
|
|
dry_run: input.dry_run,
|
|
scanned_count: 1,
|
|
matched_count: 1,
|
|
updated_count: input.dry_run ? 0 : 1,
|
|
cleaned_field_count: input.dry_run ? 2 : 1,
|
|
blocker_count: 0,
|
|
blocker_samples: [],
|
|
next_cursor: null,
|
|
has_more: false,
|
|
batch_sha256: 'a'.repeat(64),
|
|
error_message: null,
|
|
});
|
|
await expect(
|
|
scanScopes(
|
|
{ chunkSize: 25 },
|
|
{ apply: true, callProcedure: driftingCall },
|
|
),
|
|
).rejects.toThrow('清理字段数');
|
|
|
|
const residualCall = async (
|
|
_options: Record<string, unknown>,
|
|
input: Record<string, unknown>,
|
|
) => ({
|
|
ok: true,
|
|
scope: input.scope,
|
|
dry_run: true,
|
|
scanned_count: 1,
|
|
matched_count: 1,
|
|
updated_count: 0,
|
|
cleaned_field_count: 1,
|
|
blocker_count: 0,
|
|
blocker_samples: [],
|
|
next_cursor: null,
|
|
has_more: false,
|
|
batch_sha256: 'b'.repeat(64),
|
|
error_message: null,
|
|
});
|
|
await expect(
|
|
scanScopes(
|
|
{ chunkSize: 25 },
|
|
{ verifyZero: true, callProcedure: residualCall },
|
|
),
|
|
).rejects.toThrow('apply 后复核仍有');
|
|
});
|
|
|
|
it('报告游标循环时不泄露原始标识', async () => {
|
|
const privateCursor = 'private-asset-id';
|
|
const loopingCall = async (
|
|
_options: Record<string, unknown>,
|
|
input: Record<string, unknown>,
|
|
) => ({
|
|
ok: true,
|
|
scope: input.scope,
|
|
dry_run: true,
|
|
scanned_count: 1,
|
|
matched_count: 0,
|
|
updated_count: 0,
|
|
cleaned_field_count: 0,
|
|
blocker_count: 0,
|
|
blocker_samples: [],
|
|
next_cursor: privateCursor,
|
|
has_more: true,
|
|
batch_sha256: 'c'.repeat(64),
|
|
error_message: null,
|
|
});
|
|
|
|
let message = '';
|
|
try {
|
|
await scanScopes({ chunkSize: 25 }, { callProcedure: loopingCall });
|
|
} catch (error) {
|
|
message = error instanceof Error ? error.message : String(error);
|
|
}
|
|
expect(message).toContain('SHA-256');
|
|
expect(message).not.toContain(privateCursor);
|
|
});
|
|
});
|