Files
Genarrative/src/components/image-editor/perfectPixelOperationStore.test.ts
T
lhk229 8f19964e3f
Project CI / Repository checks (push) Successful in 1m0s
Project CI / Backend tests (push) Successful in 3m52s
Project CI / Native shell tests (push) Successful in 13m8s
Project CI / Frontend tests (push) Successful in 2m43s
加入手动完美像素入口;像素艺术勾选时自动补提示词约束 (#124)
Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/124
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-08-05 21:28:40 +08:00

229 lines
6.8 KiB
TypeScript

/**
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { PerfectPixelOperationSnapshot } from './ImageCanvasEditorTypes';
import {
forgetPerfectPixelOperation,
PERFECT_PIXEL_OPERATION_RETENTION_LIMIT,
PERFECT_PIXEL_OPERATION_RETENTION_MS,
readPerfectPixelOperations,
savePerfectPixelOperation,
} from './perfectPixelOperationStore';
const OWNER_USER_ID = 'user-a';
const PROJECT_ID = 'project-1';
const NOW = 1_785_715_270_000;
function buildOperation(
dialogId: string,
overrides: Partial<PerfectPixelOperationSnapshot> = {},
): PerfectPixelOperationSnapshot {
return {
version: 1,
kind: 'perfect-pixel',
operationId: dialogId,
taskId: `pixel-art-snap-${dialogId}`,
request: {
sourceImageSrc: 'ref:project-resource:resource-source',
projectId: PROJECT_ID,
sourceResourceId: 'resource-source',
assetKind: 'character',
assetLabel: '角色 · 完美像素',
canvasCompletion: {
dialogId,
title: '角色 · 完美像素',
placeholder: {
x: 100,
y: 120,
width: 320,
height: 320,
originalWidth: 640,
originalHeight: 640,
},
},
},
submittedAt: NOW,
reconcileUntil: NOW + 75_000,
...overrides,
};
}
describe('perfectPixelOperationStore', () => {
beforeEach(() => {
window.localStorage.clear();
vi.useFakeTimers();
vi.setSystemTime(NOW);
});
afterEach(() => {
vi.useRealTimers();
});
it('round-trips an operation for the same owner and project', () => {
const operation = buildOperation('dialog-1');
savePerfectPixelOperation(OWNER_USER_ID, PROJECT_ID, operation);
const ledger = readPerfectPixelOperations(OWNER_USER_ID, PROJECT_ID);
expect(ledger.get('dialog-1')).toEqual(operation);
});
it('keeps ledgers separated by project and drops another owner entirely', () => {
savePerfectPixelOperation(
OWNER_USER_ID,
PROJECT_ID,
buildOperation('dialog-1'),
);
expect(readPerfectPixelOperations(OWNER_USER_ID, 'project-2').size).toBe(0);
// 中文注释:同一台机器换账号后,上一个账号的请求(含源图直传地址)必须整条丢弃。
expect(readPerfectPixelOperations('user-b', PROJECT_ID).size).toBe(0);
expect(readPerfectPixelOperations(OWNER_USER_ID, PROJECT_ID).size).toBe(0);
});
it('refuses to store an operation whose request targets another project', () => {
savePerfectPixelOperation(
OWNER_USER_ID,
'project-2',
buildOperation('dialog-1'),
);
expect(readPerfectPixelOperations(OWNER_USER_ID, 'project-2').size).toBe(0);
});
it('fails closed on a tampered ledger entry instead of replaying it', () => {
savePerfectPixelOperation(
OWNER_USER_ID,
PROJECT_ID,
buildOperation('dialog-1'),
);
savePerfectPixelOperation(
OWNER_USER_ID,
PROJECT_ID,
buildOperation('dialog-2'),
);
const key = `genarrative.imageCanvas.perfectPixelOperations.${PROJECT_ID}`;
const stored = JSON.parse(window.localStorage.getItem(key)!) as {
ownerUserId: string;
operations: Record<string, PerfectPixelOperationSnapshot>;
};
stored.operations['dialog-1']!.taskId = 'pixel-art-snap-somewhere-else';
window.localStorage.setItem(key, JSON.stringify(stored));
const ledger = readPerfectPixelOperations(OWNER_USER_ID, PROJECT_ID);
expect(ledger.has('dialog-1')).toBe(false);
expect(ledger.has('dialog-2')).toBe(true);
});
it('drops entries past the retention window', () => {
savePerfectPixelOperation(
OWNER_USER_ID,
PROJECT_ID,
buildOperation('dialog-old', {
submittedAt: NOW - PERFECT_PIXEL_OPERATION_RETENTION_MS - 1,
reconcileUntil: NOW - PERFECT_PIXEL_OPERATION_RETENTION_MS + 74_999,
}),
);
savePerfectPixelOperation(
OWNER_USER_ID,
PROJECT_ID,
buildOperation('dialog-fresh'),
);
const ledger = readPerfectPixelOperations(OWNER_USER_ID, PROJECT_ID);
expect([...ledger.keys()]).toEqual(['dialog-fresh']);
});
it('caps the ledger size by keeping the newest submissions', () => {
for (
let index = 0;
index <= PERFECT_PIXEL_OPERATION_RETENTION_LIMIT;
index += 1
) {
savePerfectPixelOperation(
OWNER_USER_ID,
PROJECT_ID,
buildOperation(`dialog-${index}`, {
submittedAt: NOW - (PERFECT_PIXEL_OPERATION_RETENTION_LIMIT - index),
reconcileUntil:
NOW - (PERFECT_PIXEL_OPERATION_RETENTION_LIMIT - index) + 75_000,
}),
);
}
const ledger = readPerfectPixelOperations(OWNER_USER_ID, PROJECT_ID);
expect(ledger.size).toBe(PERFECT_PIXEL_OPERATION_RETENTION_LIMIT);
expect(ledger.has('dialog-0')).toBe(false);
expect(
ledger.has(`dialog-${PERFECT_PIXEL_OPERATION_RETENTION_LIMIT}`),
).toBe(true);
});
it('forgets a settled operation and clears the key once empty', () => {
savePerfectPixelOperation(
OWNER_USER_ID,
PROJECT_ID,
buildOperation('dialog-1'),
);
forgetPerfectPixelOperation(OWNER_USER_ID, PROJECT_ID, 'dialog-1');
expect(readPerfectPixelOperations(OWNER_USER_ID, PROJECT_ID).size).toBe(0);
expect(
window.localStorage.getItem(
`genarrative.imageCanvas.perfectPixelOperations.${PROJECT_ID}`,
),
).toBeNull();
});
it('degrades to an empty ledger without throwing when storage is unavailable', () => {
const setItem = vi
.spyOn(Storage.prototype, 'setItem')
.mockImplementation(() => {
throw new Error('QuotaExceededError');
});
const getItem = vi
.spyOn(Storage.prototype, 'getItem')
.mockImplementation(() => {
throw new Error('SecurityError');
});
try {
// 中文注释:隐私模式 / 配额写满时账本读写都会抛。这里必须静默降级——账本缺失只
// 意味着刷新后不能自动收口,绝不能反过来阻断发起、重试或删除。
expect(() =>
savePerfectPixelOperation(
OWNER_USER_ID,
PROJECT_ID,
buildOperation('dialog-1'),
),
).not.toThrow();
expect(() =>
forgetPerfectPixelOperation(OWNER_USER_ID, PROJECT_ID, 'dialog-1'),
).not.toThrow();
expect(readPerfectPixelOperations(OWNER_USER_ID, PROJECT_ID).size).toBe(
0,
);
} finally {
setItem.mockRestore();
getItem.mockRestore();
}
});
it('returns an empty ledger without an owner or project', () => {
savePerfectPixelOperation(
OWNER_USER_ID,
PROJECT_ID,
buildOperation('dialog-1'),
);
expect(readPerfectPixelOperations(null, PROJECT_ID).size).toBe(0);
expect(readPerfectPixelOperations(OWNER_USER_ID, ' ').size).toBe(0);
});
});