Files
Genarrative/scripts/spacetime-clean-editor-image-asset-kind.test.ts
lhk229 3d8e0211db
Project CI / Repository checks (push) Successful in 3m54s
Project CI / Frontend tests (push) Successful in 4m20s
Project CI / Backend tests (push) Successful in 4m33s
Project CI / Native shell tests (push) Successful in 16m31s
5分钟策划agent (#159)
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>
2026-08-24 21:44:44 +08:00

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);
});
});