281c84b7bf
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/142 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
667 lines
21 KiB
TypeScript
667 lines
21 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import backgroundMusicPromptCanonicalizationCases from '../../../packages/shared/test-fixtures/background-music-prompt-canonicalization.json';
|
|
import {
|
|
BACKGROUND_MUSIC_PROMPT_COMPLETION_MIN_EFFECTIVE_CODE_POINTS,
|
|
BACKGROUND_MUSIC_PROMPT_GENERATION_MIN_EFFECTIVE_CODE_POINTS,
|
|
BACKGROUND_MUSIC_PROMPT_MAX_CODE_POINTS,
|
|
BACKGROUND_MUSIC_PROMPT_SIMPLIFICATION_MAX_CODE_POINTS,
|
|
canCompleteBackgroundMusicPrompt,
|
|
canGenerateBackgroundMusicFromPrompt,
|
|
canonicalizeBackgroundMusicPrompt,
|
|
canSimplifyBackgroundMusicPrompt,
|
|
countEffectivePromptCodePoints,
|
|
countPromptCodePoints,
|
|
createBackgroundMusicPromptStateModel,
|
|
} from './ImageCanvasBackgroundMusicPromptModel';
|
|
|
|
describe('ImageCanvasBackgroundMusicPromptModel', () => {
|
|
it.each(backgroundMusicPromptCanonicalizationCases)(
|
|
'canonicalizes shared fixture case $name',
|
|
({ input, prompt, charCount, effectiveCharCount }) => {
|
|
expect(canonicalizeBackgroundMusicPrompt(input)).toBe(prompt);
|
|
expect(countPromptCodePoints(input)).toBe(charCount);
|
|
expect(countEffectivePromptCodePoints(input)).toBe(effectiveCharCount);
|
|
expect(canonicalizeBackgroundMusicPrompt(prompt)).toBe(prompt);
|
|
},
|
|
);
|
|
|
|
it('allows formal generation for 1-200 code points with effective content', () => {
|
|
expect(BACKGROUND_MUSIC_PROMPT_MAX_CODE_POINTS).toBe(200);
|
|
expect(BACKGROUND_MUSIC_PROMPT_GENERATION_MIN_EFFECTIVE_CODE_POINTS).toBe(
|
|
1,
|
|
);
|
|
expect(canGenerateBackgroundMusicFromPrompt('')).toBe(false);
|
|
expect(canGenerateBackgroundMusicFromPrompt(' \r\n\u0085')).toBe(false);
|
|
expect(canGenerateBackgroundMusicFromPrompt('乐')).toBe(true);
|
|
expect(canGenerateBackgroundMusicFromPrompt('😀'.repeat(199))).toBe(true);
|
|
expect(canGenerateBackgroundMusicFromPrompt('😀'.repeat(200))).toBe(true);
|
|
expect(canGenerateBackgroundMusicFromPrompt('😀'.repeat(201))).toBe(false);
|
|
});
|
|
|
|
it('requires 2 effective code points and at most 200 total for AI completion', () => {
|
|
expect(BACKGROUND_MUSIC_PROMPT_COMPLETION_MIN_EFFECTIVE_CODE_POINTS).toBe(
|
|
2,
|
|
);
|
|
expect(canCompleteBackgroundMusicPrompt(' \r\n\u0085')).toBe(false);
|
|
expect(canCompleteBackgroundMusicPrompt('乐')).toBe(false);
|
|
expect(canCompleteBackgroundMusicPrompt('乐 ')).toBe(false);
|
|
expect(canCompleteBackgroundMusicPrompt('乐曲')).toBe(true);
|
|
expect(canCompleteBackgroundMusicPrompt('😀'.repeat(200))).toBe(true);
|
|
expect(canCompleteBackgroundMusicPrompt('😀'.repeat(201))).toBe(false);
|
|
});
|
|
|
|
it('allows simplification only for a non-empty canonical prompt with 201-2000 code points', () => {
|
|
expect(BACKGROUND_MUSIC_PROMPT_SIMPLIFICATION_MAX_CODE_POINTS).toBe(2000);
|
|
expect(canSimplifyBackgroundMusicPrompt(' \r\n\u0085')).toBe(false);
|
|
expect(canSimplifyBackgroundMusicPrompt('A'.repeat(200))).toBe(false);
|
|
expect(canSimplifyBackgroundMusicPrompt('A'.repeat(201))).toBe(true);
|
|
expect(canSimplifyBackgroundMusicPrompt('A'.repeat(2000))).toBe(true);
|
|
expect(canSimplifyBackgroundMusicPrompt('A'.repeat(2001))).toBe(false);
|
|
});
|
|
|
|
it.each([200, 201])(
|
|
'canonicalizes %i Unicode White_Space code points to an empty disabled prompt',
|
|
(length) => {
|
|
const prompt = '\u0085'.repeat(length);
|
|
|
|
expect(canonicalizeBackgroundMusicPrompt(prompt)).toBe('');
|
|
expect(countPromptCodePoints(prompt)).toBe(0);
|
|
expect(countEffectivePromptCodePoints(prompt)).toBe(0);
|
|
expect(canGenerateBackgroundMusicFromPrompt(prompt)).toBe(false);
|
|
expect(canCompleteBackgroundMusicPrompt(prompt)).toBe(false);
|
|
expect(canSimplifyBackgroundMusicPrompt(prompt)).toBe(false);
|
|
},
|
|
);
|
|
|
|
it('uses the canonical prompt for generation, completion, and simplification states', () => {
|
|
const boundaryWhiteSpaceAroundA = `${'\u0085'.repeat(201)}A${'\u3000'.repeat(201)}`;
|
|
const overLimitWithInternalWhiteSpace = `A${' '.repeat(199)}B`;
|
|
const boundaryWhiteSpaceAround200Characters = `${'\u0085'.repeat(201)}${'A'.repeat(200)}${'\u3000'.repeat(201)}`;
|
|
|
|
expect(canonicalizeBackgroundMusicPrompt(boundaryWhiteSpaceAroundA)).toBe(
|
|
'A',
|
|
);
|
|
expect(countPromptCodePoints(boundaryWhiteSpaceAroundA)).toBe(1);
|
|
expect(countEffectivePromptCodePoints(boundaryWhiteSpaceAroundA)).toBe(1);
|
|
expect(
|
|
canGenerateBackgroundMusicFromPrompt(boundaryWhiteSpaceAroundA),
|
|
).toBe(true);
|
|
expect(canCompleteBackgroundMusicPrompt(boundaryWhiteSpaceAroundA)).toBe(
|
|
false,
|
|
);
|
|
expect(canSimplifyBackgroundMusicPrompt(boundaryWhiteSpaceAroundA)).toBe(
|
|
false,
|
|
);
|
|
|
|
expect(countPromptCodePoints(overLimitWithInternalWhiteSpace)).toBe(201);
|
|
expect(
|
|
countEffectivePromptCodePoints(overLimitWithInternalWhiteSpace),
|
|
).toBe(2);
|
|
expect(
|
|
canGenerateBackgroundMusicFromPrompt(overLimitWithInternalWhiteSpace),
|
|
).toBe(false);
|
|
expect(
|
|
canCompleteBackgroundMusicPrompt(overLimitWithInternalWhiteSpace),
|
|
).toBe(false);
|
|
expect(
|
|
canSimplifyBackgroundMusicPrompt(overLimitWithInternalWhiteSpace),
|
|
).toBe(true);
|
|
|
|
expect(countPromptCodePoints(boundaryWhiteSpaceAround200Characters)).toBe(
|
|
200,
|
|
);
|
|
expect(
|
|
countEffectivePromptCodePoints(boundaryWhiteSpaceAround200Characters),
|
|
).toBe(200);
|
|
expect(
|
|
canGenerateBackgroundMusicFromPrompt(
|
|
boundaryWhiteSpaceAround200Characters,
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
canCompleteBackgroundMusicPrompt(boundaryWhiteSpaceAround200Characters),
|
|
).toBe(true);
|
|
expect(
|
|
canSimplifyBackgroundMusicPrompt(boundaryWhiteSpaceAround200Characters),
|
|
).toBe(false);
|
|
});
|
|
|
|
it.each(['\u200B', '\uFEFF'])(
|
|
'keeps 201 %s code points and enables only simplification',
|
|
(codePoint) => {
|
|
const prompt = codePoint.repeat(201);
|
|
|
|
expect(canonicalizeBackgroundMusicPrompt(prompt)).toBe(prompt);
|
|
expect(countPromptCodePoints(prompt)).toBe(201);
|
|
expect(countEffectivePromptCodePoints(prompt)).toBe(201);
|
|
expect(canGenerateBackgroundMusicFromPrompt(prompt)).toBe(false);
|
|
expect(canCompleteBackgroundMusicPrompt(prompt)).toBe(false);
|
|
expect(canSimplifyBackgroundMusicPrompt(prompt)).toBe(true);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe('background music Prompt dialog state model', () => {
|
|
it('claims an operation synchronously, ignores same-action double clicks, and invalidates an older operation', () => {
|
|
const model = createBackgroundMusicPromptStateModel();
|
|
const completion = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: ' \u0085森林冒险\u3000',
|
|
});
|
|
|
|
expect(completion).toMatchObject({
|
|
started: true,
|
|
reason: 'started',
|
|
canonicalPrompt: '森林冒险',
|
|
state: {
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
undoPromptSnapshot: null,
|
|
temporaryPromptSnapshot: '森林冒险',
|
|
},
|
|
});
|
|
expect(completion.operation).not.toBeNull();
|
|
|
|
const duplicate = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: '另一份有效输入',
|
|
});
|
|
expect(duplicate).toMatchObject({
|
|
started: false,
|
|
reason: 'duplicate',
|
|
canonicalPrompt: '森林冒险',
|
|
operation: null,
|
|
});
|
|
expect(duplicate.state.operationId).toBe(completion.operation?.operationId);
|
|
|
|
const simplification = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'simplifying',
|
|
prompt: '长'.repeat(201),
|
|
});
|
|
expect(simplification).toMatchObject({
|
|
started: true,
|
|
reason: 'started',
|
|
canonicalPrompt: '长'.repeat(201),
|
|
state: {
|
|
status: 'simplifying',
|
|
temporaryPromptSnapshot: '长'.repeat(201),
|
|
},
|
|
});
|
|
expect(simplification.operation?.operationId).not.toBe(
|
|
completion.operation?.operationId,
|
|
);
|
|
|
|
if (!completion.operation) {
|
|
throw new Error('expected completion operation');
|
|
}
|
|
expect(
|
|
model.resolveAiOperation(completion.operation, '迟到的补全结果'),
|
|
).toMatchObject({
|
|
applied: false,
|
|
prompt: null,
|
|
state: {
|
|
status: 'simplifying',
|
|
operationId: simplification.operation?.operationId,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('isolates active operations and snapshots by dialog ID', () => {
|
|
const model = createBackgroundMusicPromptStateModel();
|
|
const completion = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: '森林冒险',
|
|
});
|
|
const simplification = model.beginOperation({
|
|
dialogId: 'dialog-b',
|
|
status: 'simplifying',
|
|
prompt: '长'.repeat(201),
|
|
});
|
|
if (!completion.operation || !simplification.operation) {
|
|
throw new Error('expected both dialog operations');
|
|
}
|
|
|
|
expect(
|
|
model.resolveAiOperation(completion.operation, '森林冒险补全结果'),
|
|
).toMatchObject({
|
|
applied: true,
|
|
prompt: '森林冒险补全结果',
|
|
state: {
|
|
dialogId: 'dialog-a',
|
|
status: 'idle',
|
|
undoPromptSnapshot: '森林冒险',
|
|
},
|
|
});
|
|
expect(model.getDialogState('dialog-b')).toMatchObject({
|
|
status: 'simplifying',
|
|
operationId: simplification.operation.operationId,
|
|
temporaryPromptSnapshot: '长'.repeat(201),
|
|
});
|
|
|
|
expect(
|
|
model.resolveAiOperation(
|
|
{ ...completion.operation, dialogId: 'dialog-b' },
|
|
'不能写入另一个面板',
|
|
),
|
|
).toMatchObject({
|
|
applied: false,
|
|
prompt: null,
|
|
state: {
|
|
dialogId: 'dialog-b',
|
|
status: 'simplifying',
|
|
operationId: simplification.operation.operationId,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('keeps a canonical temporary snapshot and promotes it only after a successful AI writeback', () => {
|
|
const model = createBackgroundMusicPromptStateModel();
|
|
const start = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: ' \u0085森林\r\n冒险\u3000',
|
|
});
|
|
if (!start.operation) {
|
|
throw new Error('expected completion operation');
|
|
}
|
|
|
|
expect(start.canonicalPrompt).toBe('森林\r\n冒险');
|
|
expect(start.state).toMatchObject({
|
|
status: 'completing',
|
|
undoPromptSnapshot: null,
|
|
temporaryPromptSnapshot: '森林\r\n冒险',
|
|
});
|
|
|
|
expect(
|
|
model.resolveAiOperation(start.operation, ' \u0085温暖森林冒险\u3000'),
|
|
).toEqual({
|
|
applied: true,
|
|
prompt: '温暖森林冒险',
|
|
state: {
|
|
dialogId: 'dialog-a',
|
|
status: 'idle',
|
|
operationId: null,
|
|
undoPromptSnapshot: '森林\r\n冒险',
|
|
temporaryPromptSnapshot: null,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('clears the current temporary snapshot on failure without restoring the replaced undo snapshot', () => {
|
|
const model = createBackgroundMusicPromptStateModel();
|
|
const first = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: '初始描述',
|
|
});
|
|
if (!first.operation) {
|
|
throw new Error('expected first completion operation');
|
|
}
|
|
model.resolveAiOperation(first.operation, '第一次补全结果');
|
|
expect(model.getDialogState('dialog-a').undoPromptSnapshot).toBe(
|
|
'初始描述',
|
|
);
|
|
|
|
const second = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: ' \u0085用户手动修改\u3000',
|
|
});
|
|
if (!second.operation) {
|
|
throw new Error('expected second completion operation');
|
|
}
|
|
expect(second.state).toMatchObject({
|
|
undoPromptSnapshot: null,
|
|
temporaryPromptSnapshot: '用户手动修改',
|
|
});
|
|
|
|
expect(model.rejectOperation(second.operation)).toEqual({
|
|
applied: true,
|
|
prompt: '用户手动修改',
|
|
state: {
|
|
dialogId: 'dialog-a',
|
|
status: 'idle',
|
|
operationId: null,
|
|
undoPromptSnapshot: null,
|
|
temporaryPromptSnapshot: null,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('treats an invalid AI success payload as a failed writeback and never exposes it', () => {
|
|
const model = createBackgroundMusicPromptStateModel();
|
|
const start = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: '森林冒险',
|
|
});
|
|
if (!start.operation) {
|
|
throw new Error('expected completion operation');
|
|
}
|
|
|
|
expect(model.resolveAiOperation(start.operation, ' \u0085\u3000')).toEqual({
|
|
applied: false,
|
|
prompt: null,
|
|
state: {
|
|
dialogId: 'dialog-a',
|
|
status: 'idle',
|
|
operationId: null,
|
|
undoPromptSnapshot: null,
|
|
temporaryPromptSnapshot: null,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('swaps one canonical undo snapshot repeatedly after manual edits', () => {
|
|
const model = createBackgroundMusicPromptStateModel();
|
|
const start = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: '原始描述',
|
|
});
|
|
if (!start.operation) {
|
|
throw new Error('expected completion operation');
|
|
}
|
|
model.resolveAiOperation(start.operation, 'AI 补全结果');
|
|
|
|
const firstUndo = model.swapUndoSnapshot(
|
|
'dialog-a',
|
|
' \u0085用户修改后的结果\u3000',
|
|
);
|
|
expect(firstUndo).toEqual({
|
|
applied: true,
|
|
prompt: '原始描述',
|
|
state: {
|
|
dialogId: 'dialog-a',
|
|
status: 'idle',
|
|
operationId: null,
|
|
undoPromptSnapshot: '用户修改后的结果',
|
|
temporaryPromptSnapshot: null,
|
|
},
|
|
});
|
|
|
|
const secondUndo = model.swapUndoSnapshot(
|
|
'dialog-a',
|
|
' \u0085原始描述\u3000',
|
|
);
|
|
expect(secondUndo).toEqual({
|
|
applied: true,
|
|
prompt: '用户修改后的结果',
|
|
state: {
|
|
dialogId: 'dialog-a',
|
|
status: 'idle',
|
|
operationId: null,
|
|
undoPromptSnapshot: '原始描述',
|
|
temporaryPromptSnapshot: null,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('clears the undo snapshot at the preset boundary', () => {
|
|
const model = createBackgroundMusicPromptStateModel();
|
|
const first = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: '原始描述',
|
|
});
|
|
if (!first.operation) {
|
|
throw new Error('expected first completion operation');
|
|
}
|
|
model.resolveAiOperation(first.operation, '第一次补全结果');
|
|
expect(model.getDialogState('dialog-a').undoPromptSnapshot).toBe(
|
|
'原始描述',
|
|
);
|
|
|
|
expect(
|
|
model.preparePreset('dialog-a', ' \u0085第一次补全结果\u3000'),
|
|
).toEqual({
|
|
applied: true,
|
|
prompt: '第一次补全结果',
|
|
state: {
|
|
dialogId: 'dialog-a',
|
|
status: 'idle',
|
|
operationId: null,
|
|
undoPromptSnapshot: null,
|
|
temporaryPromptSnapshot: null,
|
|
},
|
|
});
|
|
|
|
const pending = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: '第一次补全结果',
|
|
});
|
|
if (!pending.operation) {
|
|
throw new Error('expected pending completion operation');
|
|
}
|
|
expect(model.preparePreset('dialog-a', '处理期间应用新预设')).toMatchObject(
|
|
{
|
|
applied: true,
|
|
prompt: '处理期间应用新预设',
|
|
state: {
|
|
status: 'idle',
|
|
operationId: null,
|
|
undoPromptSnapshot: null,
|
|
temporaryPromptSnapshot: null,
|
|
},
|
|
},
|
|
);
|
|
expect(
|
|
model.resolveAiOperation(pending.operation, '不应写回的迟到结果'),
|
|
).toMatchObject({
|
|
applied: false,
|
|
prompt: null,
|
|
});
|
|
});
|
|
|
|
it('preserves the existing undo snapshot when a formal submission is rejected', () => {
|
|
const model = createBackgroundMusicPromptStateModel();
|
|
const completion = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: '原始描述',
|
|
});
|
|
if (!completion.operation) {
|
|
throw new Error('expected completion operation');
|
|
}
|
|
model.resolveAiOperation(completion.operation, 'AI 补全结果');
|
|
|
|
const submission = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'submitting',
|
|
prompt: ' \u0085AI 补全结果\u3000',
|
|
});
|
|
if (!submission.operation) {
|
|
throw new Error('expected submission operation');
|
|
}
|
|
expect(submission).toMatchObject({
|
|
started: true,
|
|
canonicalPrompt: 'AI 补全结果',
|
|
state: {
|
|
status: 'submitting',
|
|
undoPromptSnapshot: '原始描述',
|
|
temporaryPromptSnapshot: null,
|
|
},
|
|
});
|
|
expect(
|
|
model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'submitting',
|
|
prompt: 'AI 补全结果',
|
|
}),
|
|
).toMatchObject({
|
|
started: false,
|
|
reason: 'duplicate',
|
|
});
|
|
|
|
expect(
|
|
model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: '不能覆盖正式提交',
|
|
}),
|
|
).toMatchObject({
|
|
started: false,
|
|
reason: 'locked',
|
|
canonicalPrompt: 'AI 补全结果',
|
|
operation: null,
|
|
state: {
|
|
status: 'submitting',
|
|
operationId: submission.operation.operationId,
|
|
undoPromptSnapshot: '原始描述',
|
|
},
|
|
});
|
|
expect(
|
|
model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'simplifying',
|
|
prompt: '长'.repeat(201),
|
|
}),
|
|
).toMatchObject({
|
|
started: false,
|
|
reason: 'locked',
|
|
canonicalPrompt: 'AI 补全结果',
|
|
operation: null,
|
|
state: {
|
|
status: 'submitting',
|
|
operationId: submission.operation.operationId,
|
|
},
|
|
});
|
|
expect(
|
|
model.preparePreset('dialog-a', '不能用预设取消正式提交'),
|
|
).toMatchObject({
|
|
applied: false,
|
|
prompt: 'AI 补全结果',
|
|
state: {
|
|
status: 'submitting',
|
|
operationId: submission.operation.operationId,
|
|
undoPromptSnapshot: '原始描述',
|
|
},
|
|
});
|
|
|
|
expect(model.rejectOperation(submission.operation)).toEqual({
|
|
applied: true,
|
|
prompt: 'AI 补全结果',
|
|
state: {
|
|
dialogId: 'dialog-a',
|
|
status: 'idle',
|
|
operationId: null,
|
|
undoPromptSnapshot: '原始描述',
|
|
temporaryPromptSnapshot: null,
|
|
},
|
|
});
|
|
|
|
const acceptedSubmission = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'submitting',
|
|
prompt: 'AI 补全结果',
|
|
});
|
|
if (!acceptedSubmission.operation) {
|
|
throw new Error('expected accepted submission operation');
|
|
}
|
|
expect(
|
|
model.completeSubmittingOperation(acceptedSubmission.operation),
|
|
).toEqual({
|
|
applied: true,
|
|
prompt: 'AI 补全结果',
|
|
state: {
|
|
dialogId: 'dialog-a',
|
|
status: 'idle',
|
|
operationId: null,
|
|
undoPromptSnapshot: '原始描述',
|
|
temporaryPromptSnapshot: null,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('accepts simplification at 2000 code points and rejects 2001 without deleting the canonical prompt', () => {
|
|
const model = createBackgroundMusicPromptStateModel();
|
|
const atLimitPrompt = '长'.repeat(2000);
|
|
const overLimitPrompt = '长'.repeat(2001);
|
|
|
|
expect(
|
|
model.beginOperation({
|
|
dialogId: 'dialog-at-limit',
|
|
status: 'simplifying',
|
|
prompt: ` \u0085${atLimitPrompt}\u3000`,
|
|
}),
|
|
).toMatchObject({
|
|
started: true,
|
|
reason: 'started',
|
|
canonicalPrompt: atLimitPrompt,
|
|
state: {
|
|
status: 'simplifying',
|
|
temporaryPromptSnapshot: atLimitPrompt,
|
|
},
|
|
});
|
|
|
|
expect(
|
|
model.beginOperation({
|
|
dialogId: 'dialog-over-limit',
|
|
status: 'simplifying',
|
|
prompt: ` \u0085${overLimitPrompt}\u3000`,
|
|
}),
|
|
).toEqual({
|
|
started: false,
|
|
reason: 'ineligible',
|
|
canonicalPrompt: overLimitPrompt,
|
|
operation: null,
|
|
state: {
|
|
dialogId: 'dialog-over-limit',
|
|
status: 'idle',
|
|
operationId: null,
|
|
undoPromptSnapshot: null,
|
|
temporaryPromptSnapshot: null,
|
|
},
|
|
});
|
|
expect(canCompleteBackgroundMusicPrompt(overLimitPrompt)).toBe(false);
|
|
expect(canGenerateBackgroundMusicFromPrompt(overLimitPrompt)).toBe(false);
|
|
expect(canSimplifyBackgroundMusicPrompt(overLimitPrompt)).toBe(false);
|
|
});
|
|
|
|
it('invalidates a closed dialog operation even if the same dialog ID is reopened', () => {
|
|
const model = createBackgroundMusicPromptStateModel();
|
|
const closedOperation = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: '关闭前描述',
|
|
});
|
|
if (!closedOperation.operation) {
|
|
throw new Error('expected completion operation');
|
|
}
|
|
|
|
model.closeDialog('dialog-a');
|
|
expect(model.getDialogState('dialog-a')).toEqual({
|
|
dialogId: 'dialog-a',
|
|
status: 'idle',
|
|
operationId: null,
|
|
undoPromptSnapshot: null,
|
|
temporaryPromptSnapshot: null,
|
|
});
|
|
|
|
const reopenedOperation = model.beginOperation({
|
|
dialogId: 'dialog-a',
|
|
status: 'completing',
|
|
prompt: '重新打开描述',
|
|
});
|
|
expect(reopenedOperation.operation?.operationId).not.toBe(
|
|
closedOperation.operation.operationId,
|
|
);
|
|
expect(
|
|
model.resolveAiOperation(closedOperation.operation, '关闭后的迟到结果'),
|
|
).toMatchObject({
|
|
applied: false,
|
|
prompt: null,
|
|
state: {
|
|
status: 'completing',
|
|
operationId: reopenedOperation.operation?.operationId,
|
|
},
|
|
});
|
|
});
|
|
});
|