feat(worker): watermark versioning with atomic history regeneration

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-20 17:53:44 +08:00
parent 6723a0ce88
commit 243b2f8721
34 changed files with 3057 additions and 78 deletions
@@ -0,0 +1,121 @@
// packages/contracts/src/__tests__/watermark.test.ts —— 水印配置共享形状测试
// (P4a Task 3):normalize 填齐缺省不改已给值;parse 对脏数据的防御回退;
// buildAlbumUpdatedPayloadCore 的水印副本位(watermarkConfigVersion = 有效版本)
// 与翻译排序确定性。这三者是 api/worker 双侧共用的唯一权威,漂移即事故。
import {
normalizeWatermarkConfig,
parseStoredWatermarkConfig,
WATERMARK_DEFAULT_POSITION,
WATERMARK_DEFAULT_OPACITY,
WATERMARK_DEFAULT_SCALE_PERCENT,
WATERMARK_DEFAULT_FONT_SIZE,
WATERMARK_POSITIONS,
buildAlbumUpdatedPayloadCore,
type AlbumPayloadRow,
} from '../index.js';
describe('normalizeWatermarkConfig', () => {
it('填齐全部缺省(与 openapi WatermarkConfig 可选字段一一对应)', () => {
expect(normalizeWatermarkConfig({ enabled: true, text: { content: 'x' } })).toEqual({
enabled: true,
position: WATERMARK_DEFAULT_POSITION,
opacity: WATERMARK_DEFAULT_OPACITY,
scalePercent: WATERMARK_DEFAULT_SCALE_PERCENT,
text: { content: 'x', fontSize: WATERMARK_DEFAULT_FONT_SIZE },
});
});
it('不改写已给值;enabled=false 允许无文字(关闭语义仅回显)', () => {
expect(
normalizeWatermarkConfig({
enabled: false,
position: 'topLeft',
opacity: 80,
scalePercent: 30,
text: { content: '', fontSize: 40 },
}),
).toEqual({
enabled: false,
position: 'topLeft',
opacity: 80,
scalePercent: 30,
text: { content: '', fontSize: 40 },
});
expect(normalizeWatermarkConfig({ enabled: false })).toMatchObject({
text: { content: '' },
});
});
});
describe('parseStoredWatermarkConfig', () => {
it('round-trip:normalize 后的值原样读回', () => {
const stored = normalizeWatermarkConfig({
enabled: true,
position: 'center',
opacity: 10,
scalePercent: 50,
text: { content: '水印', fontSize: 72 },
});
expect(parseStoredWatermarkConfig(stored)).toEqual(stored);
});
it('脏数据防御:非对象 / enabled 非 boolean → null(「配置不可用」)', () => {
expect(parseStoredWatermarkConfig(null)).toBeNull();
expect(parseStoredWatermarkConfig('enabled')).toBeNull();
expect(parseStoredWatermarkConfig({ position: 'center' })).toBeNull();
expect(parseStoredWatermarkConfig({ enabled: 'yes' })).toBeNull();
});
it('缺字段回落缺省;非法 position 回落缺省(不抛错)', () => {
expect(parseStoredWatermarkConfig({ enabled: true })).toEqual(
normalizeWatermarkConfig({ enabled: true }),
);
const badPosition = parseStoredWatermarkConfig({
enabled: true,
position: 'middle',
text: { content: 'x' },
});
expect(badPosition?.position).toBe(WATERMARK_DEFAULT_POSITION);
expect(WATERMARK_POSITIONS).not.toContain('middle');
});
});
describe('buildAlbumUpdatedPayloadCore', () => {
const album: AlbumPayloadRow = {
id: 'album-1',
publicToken: 'pt',
status: 'active',
uploadEnabled: true,
publicEnabled: true,
downloadEnabled: false,
originalDownloadEnabled: false,
watermarkEnabled: true,
watermarkEffectiveVersion: 3,
defaultLocale: 'zh-CN',
timezone: 'Asia/Shanghai',
startAt: new Date('2026-11-01T10:00:00Z'),
endAt: null,
entityVersion: 7,
};
it('水印副本位 = 有效版本(不是最新创建版本);翻译 locale 升序', () => {
const payload = buildAlbumUpdatedPayloadCore(album, [
{ locale: 'en', title: 'T', description: null, venue: null, address: null },
{ locale: 'zh-CN', title: '标题', description: null, venue: null, address: null },
]);
expect(payload.watermarkEnabled).toBe(true);
expect(payload.watermarkConfigVersion).toBe(3);
expect(payload.translations.map((t) => t.locale)).toEqual(['en', 'zh-CN']);
expect(payload.entityVersion).toBe(7);
expect(payload.endAt).toBeNull();
});
it('关闭态:enabled=false 携带版本(ov 回落 browse 键的判定依据)', () => {
const payload = buildAlbumUpdatedPayloadCore(
{ ...album, watermarkEnabled: false, watermarkEffectiveVersion: 4 },
[],
);
expect(payload.watermarkEnabled).toBe(false);
expect(payload.watermarkConfigVersion).toBe(4);
});
});