- Add prompt garden inspiration suggestions for image workspaces - Use public API paths and locale-aware suggestion loading - Preserve category paths when importing favorites from Prompt Garden
2613 lines
85 KiB
TypeScript
2613 lines
85 KiB
TypeScript
import { describe, it, expect, afterEach, vi } from 'vitest'
|
|
import { effectScope, ref } from 'vue'
|
|
import type {
|
|
LocationQuery,
|
|
RouteLocationNormalizedLoaded,
|
|
RouteLocationRaw,
|
|
Router,
|
|
} from 'vue-router'
|
|
import type { ConversationMessage, FavoritePrompt, PromptRecord, PromptRecordChain } from '@prompt-optimizer/core'
|
|
import type { MessageReactive } from 'naive-ui'
|
|
|
|
import { createTestPinia } from '../../utils/pinia-test-helpers'
|
|
import { useBasicSystemSession } from '../../../src/stores/session/useBasicSystemSession'
|
|
import { useBasicUserSession } from '../../../src/stores/session/useBasicUserSession'
|
|
import { useProMultiMessageSession } from '../../../src/stores/session/useProMultiMessageSession'
|
|
import { useProVariableSession } from '../../../src/stores/session/useProVariableSession'
|
|
import { useImageText2ImageSession } from '../../../src/stores/session/useImageText2ImageSession'
|
|
import { useImageImage2ImageSession } from '../../../src/stores/session/useImageImage2ImageSession'
|
|
import { useImageMultiImageSession } from '../../../src/stores/session/useImageMultiImageSession'
|
|
import { useAppPromptGardenImport } from '../../../src/composables/app/useAppPromptGardenImport'
|
|
import { setGlobalMessageApi } from '../../../src/composables/ui/useToast'
|
|
import { i18n } from '../../../src/plugins/i18n'
|
|
|
|
const buildFullPath = (path: string, query: LocationQuery): string => {
|
|
const params = new URLSearchParams()
|
|
for (const [key, value] of Object.entries(query)) {
|
|
if (typeof value === 'string') {
|
|
params.append(key, value)
|
|
continue
|
|
}
|
|
if (Array.isArray(value)) {
|
|
for (const v of value) {
|
|
if (typeof v === 'string') params.append(key, v)
|
|
}
|
|
}
|
|
}
|
|
const qs = params.toString()
|
|
return qs ? `${path}?${qs}` : path
|
|
}
|
|
|
|
const makeRoute = (path: string, query: LocationQuery): RouteLocationNormalizedLoaded => {
|
|
return {
|
|
fullPath: buildFullPath(path, query),
|
|
hash: '',
|
|
query,
|
|
params: {},
|
|
name: undefined,
|
|
path,
|
|
meta: {},
|
|
matched: [],
|
|
redirectedFrom: undefined,
|
|
}
|
|
}
|
|
|
|
const makeDummyRecord = (): PromptRecord => {
|
|
return {
|
|
id: 'v1',
|
|
originalPrompt: 'orig',
|
|
optimizedPrompt: 'opt',
|
|
type: 'optimize',
|
|
chainId: 'chain',
|
|
version: 1,
|
|
timestamp: Date.now(),
|
|
modelKey: 'mock-model',
|
|
templateId: 'mock-template',
|
|
}
|
|
}
|
|
|
|
const applyNavigation = (
|
|
currentRoute: { value: RouteLocationNormalizedLoaded },
|
|
to: RouteLocationRaw
|
|
) => {
|
|
if (typeof to === 'string') {
|
|
currentRoute.value = makeRoute(to, {})
|
|
return
|
|
}
|
|
|
|
if (to && typeof to === 'object' && 'path' in to && typeof to.path === 'string') {
|
|
const nextQuery = (to as { query?: unknown }).query
|
|
const query = (nextQuery && typeof nextQuery === 'object' ? (nextQuery as LocationQuery) : {})
|
|
currentRoute.value = makeRoute(to.path, query)
|
|
return
|
|
}
|
|
|
|
throw new Error('Unsupported navigation payload')
|
|
}
|
|
|
|
const waitForCondition = async (predicate: () => boolean, timeoutMs = 1500) => {
|
|
const start = Date.now()
|
|
while (!predicate()) {
|
|
if (Date.now() - start > timeoutMs) {
|
|
throw new Error('Timed out waiting for condition')
|
|
}
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
}
|
|
}
|
|
|
|
describe('useAppPromptGardenImport', () => {
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it('imports once when hasRestoredInitialState flips to true', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
// Avoid console.warn from useToast (tests fail on console.warn).
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
// Seed non-empty state so we can verify it gets cleared.
|
|
basicSystemSession.updatePrompt('old')
|
|
basicSystemSession.updateOptimizedResult({
|
|
optimizedPrompt: 'old-opt',
|
|
reasoning: 'old-r',
|
|
chainId: 'old-chain',
|
|
versionId: 'old-version',
|
|
})
|
|
basicSystemSession.updateTestContent('old-test')
|
|
basicSystemSession.testVariantResults = {
|
|
...basicSystemSession.testVariantResults,
|
|
a: { result: 'old-orig', reasoning: 'old-orig-r' },
|
|
b: { result: 'old-opt', reasoning: 'old-opt-r' },
|
|
}
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([makeDummyRecord()])
|
|
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-001',
|
|
subModeKey: 'basic-system',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/user', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'basic-system' },
|
|
prompt: { format: 'text', text: 'IMPORTED' },
|
|
variables: [],
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
}
|
|
)
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
// Still restoring: should not fetch yet.
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
|
|
// Restore completes without route changes: import should still happen.
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
expect(fetchMock.mock.calls[0]?.[0]).toBe('http://garden.local/api/public/prompt-source/NB-001')
|
|
|
|
// Navigated to target workspace.
|
|
expect(currentRoute.value.path).toBe('/basic/system')
|
|
|
|
// Session updated + cleared.
|
|
expect(basicSystemSession.prompt).toBe('IMPORTED')
|
|
expect(basicSystemSession.optimizedPrompt).toBe('')
|
|
expect(basicSystemSession.reasoning).toBe('')
|
|
expect(basicSystemSession.chainId).toBe('')
|
|
expect(basicSystemSession.versionId).toBe('')
|
|
expect(basicSystemSession.testContent).toBe('')
|
|
expect(basicSystemSession.testVariantResults.a).toEqual({ result: '', reasoning: '' })
|
|
expect(basicSystemSession.testVariantResults.b).toEqual({ result: '', reasoning: '' })
|
|
expect(optimizerCurrentVersions.value).toEqual([])
|
|
|
|
// Import params removed from the URL.
|
|
expect(currentRoute.value.query.importCode).toBeUndefined()
|
|
expect(currentRoute.value.query.subModeKey).toBeUndefined()
|
|
|
|
// External loading flag reset.
|
|
expect(isLoadingExternalData.value).toBe(false)
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('imports v1 schema messages + variables into pro-multi', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
// Avoid console.warn from useToast (tests fail on console.warn).
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
// Seed some state to ensure import resets pro-multi-specific fields.
|
|
proMultiMessageSession.setMessageChainMap({ old: 'chain' })
|
|
proMultiMessageSession.updateOptimizedResult({
|
|
optimizedPrompt: 'old-opt',
|
|
reasoning: 'old-r',
|
|
chainId: 'old-chain',
|
|
versionId: 'old-version',
|
|
})
|
|
proMultiMessageSession.testVariantResults = {
|
|
...proMultiMessageSession.testVariantResults,
|
|
a: { result: 'old-orig', reasoning: 'old-orig-r' },
|
|
b: { result: 'old-opt', reasoning: 'old-opt-r' },
|
|
}
|
|
proMultiMessageSession.setTemporaryVariable('topic', 'pizza')
|
|
proMultiMessageSession.setTemporaryVariable('obsolete', 'should-delete')
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([makeDummyRecord()])
|
|
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-PRO-001',
|
|
// subModeKey intentionally omitted to exercise v1 optimizerTarget.subModeKey.
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const v1Payload = {
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'pro-multi' },
|
|
prompt: {
|
|
format: 'messages',
|
|
messages: [
|
|
{
|
|
id: 'm1',
|
|
role: 'system',
|
|
content: 'You are a {{topic}} expert',
|
|
originalContent: 'You are a {{topic}} expert',
|
|
},
|
|
{
|
|
id: 'm2',
|
|
role: 'assistant',
|
|
content: 'OK',
|
|
originalContent: 'OK',
|
|
},
|
|
{
|
|
id: 'm3',
|
|
role: 'user',
|
|
content: 'Write it in {{format}} with a {{tone}} vibe',
|
|
originalContent: 'Write it in {{format}} with a {{tone}} vibe',
|
|
},
|
|
],
|
|
},
|
|
variables: [
|
|
{ name: 'topic', defaultValue: 'ice cream' },
|
|
{ name: 'format', defaultValue: 'markdown' },
|
|
{ name: 'tone' },
|
|
],
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(JSON.stringify(v1Payload), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
})
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
expect(fetchMock.mock.calls[0]?.[0]).toBe('http://garden.local/api/public/prompt-source/NB-PRO-001')
|
|
|
|
// Navigated to target workspace.
|
|
expect(currentRoute.value.path).toBe('/pro/multi')
|
|
|
|
const expectedMessages: ConversationMessage[] = [
|
|
{
|
|
id: 'm1',
|
|
role: 'system',
|
|
content: 'You are a {{topic}} expert',
|
|
originalContent: 'You are a {{topic}} expert',
|
|
},
|
|
{
|
|
id: 'm2',
|
|
role: 'assistant',
|
|
content: 'OK',
|
|
originalContent: 'OK',
|
|
},
|
|
{
|
|
id: 'm3',
|
|
role: 'user',
|
|
content: 'Write it in {{format}} with a {{tone}} vibe',
|
|
originalContent: 'Write it in {{format}} with a {{tone}} vibe',
|
|
},
|
|
]
|
|
|
|
// Session updated + persisted snapshot updated.
|
|
expect(proMultiMessageSession.conversationMessagesSnapshot).toEqual(expectedMessages)
|
|
|
|
// Auto-select latest system/user message.
|
|
expect(proMultiMessageSession.selectedMessageId).toBe('m3')
|
|
|
|
// Pro-multi state reset.
|
|
expect(proMultiMessageSession.messageChainMap).toEqual({})
|
|
expect(proMultiMessageSession.testVariantResults.a).toEqual({ result: '', reasoning: '' })
|
|
expect(proMultiMessageSession.testVariantResults.b).toEqual({ result: '', reasoning: '' })
|
|
expect(proMultiMessageSession.optimizedPrompt).toBe('')
|
|
expect(proMultiMessageSession.reasoning).toBe('')
|
|
expect(proMultiMessageSession.chainId).toBe('')
|
|
expect(proMultiMessageSession.versionId).toBe('')
|
|
|
|
// Variables are re-seeded from the import payload after clear-content runs.
|
|
expect(proMultiMessageSession.getTemporaryVariable('topic')).toBe('ice cream')
|
|
expect(proMultiMessageSession.getTemporaryVariable('format')).toBe('markdown')
|
|
expect(proMultiMessageSession.getTemporaryVariable('tone')).toBe('')
|
|
expect(proMultiMessageSession.getTemporaryVariable('obsolete')).toBeUndefined()
|
|
|
|
// Import params removed from the URL.
|
|
expect(currentRoute.value.query.importCode).toBeUndefined()
|
|
expect(currentRoute.value.query.subModeKey).toBeUndefined()
|
|
|
|
expect(isLoadingExternalData.value).toBe(false)
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('imports v1 schema text + variables into pro-variable', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
// Avoid console.warn from useToast (tests fail on console.warn).
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
// Seed non-empty state so we can verify it gets cleared.
|
|
proVariableSession.updatePrompt('old')
|
|
proVariableSession.updateOptimizedResult({
|
|
optimizedPrompt: 'old-opt',
|
|
reasoning: 'old-r',
|
|
chainId: 'old-chain',
|
|
versionId: 'old-version',
|
|
})
|
|
proVariableSession.updateTestContent('old-test')
|
|
proVariableSession.testVariantResults = {
|
|
...proVariableSession.testVariantResults,
|
|
a: { result: 'old-orig', reasoning: 'old-orig-r' },
|
|
b: { result: 'old-opt', reasoning: 'old-opt-r' },
|
|
}
|
|
|
|
// Existing values should be preserved.
|
|
proVariableSession.setTemporaryVariable('name', 'Bob')
|
|
proVariableSession.setTemporaryVariable('obsolete', 'should-delete')
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([makeDummyRecord()])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-PVAR-001',
|
|
// subModeKey intentionally omitted to exercise v1 optimizerTarget.subModeKey.
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const v1Payload = {
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'pro-variable' },
|
|
prompt: {
|
|
format: 'text',
|
|
text: 'Hello {{name}}',
|
|
},
|
|
variables: [
|
|
{ name: 'name', defaultValue: 'Alice' },
|
|
{ name: 'tone' },
|
|
],
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(JSON.stringify(v1Payload), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
})
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
expect(fetchMock.mock.calls[0]?.[0]).toBe('http://garden.local/api/public/prompt-source/NB-PVAR-001')
|
|
|
|
expect(currentRoute.value.path).toBe('/pro/variable')
|
|
|
|
// Prompt imported into pro-variable session.
|
|
expect(proVariableSession.prompt).toBe('Hello {{name}}')
|
|
|
|
// Session cleared.
|
|
expect(proVariableSession.optimizedPrompt).toBe('')
|
|
expect(proVariableSession.reasoning).toBe('')
|
|
expect(proVariableSession.chainId).toBe('')
|
|
expect(proVariableSession.versionId).toBe('')
|
|
expect(proVariableSession.testContent).toBe('')
|
|
expect(proVariableSession.testVariantResults.a).toEqual({ result: '', reasoning: '' })
|
|
expect(proVariableSession.testVariantResults.b).toEqual({ result: '', reasoning: '' })
|
|
|
|
// Variables are re-seeded from the import payload after clear-content runs.
|
|
expect(proVariableSession.getTemporaryVariable('name')).toBe('Alice')
|
|
expect(proVariableSession.getTemporaryVariable('tone')).toBe('')
|
|
expect(proVariableSession.getTemporaryVariable('obsolete')).toBeUndefined()
|
|
|
|
// Pro-variable import should not mutate pro-multi session messages.
|
|
expect(proMultiMessageSession.conversationMessagesSnapshot).toEqual([])
|
|
|
|
// Import params removed from the URL.
|
|
expect(currentRoute.value.query.importCode).toBeUndefined()
|
|
expect(currentRoute.value.query.subModeKey).toBeUndefined()
|
|
expect(isLoadingExternalData.value).toBe(false)
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('imports v1 schema text + variables into multiimage workspace', async () => {
|
|
const { pinia } = createTestPinia({
|
|
imageStorageService: {
|
|
getMetadata: async () => null,
|
|
saveImage: async () => {},
|
|
listAllMetadata: async () => [],
|
|
deleteImages: async () => {},
|
|
} as unknown as never,
|
|
})
|
|
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
const imageMultiImageSession = useImageMultiImageSession(pinia)
|
|
|
|
imageMultiImageSession.updatePrompt('旧多图提示词')
|
|
imageMultiImageSession.updateOptimizedResult({
|
|
optimizedPrompt: '旧优化结果',
|
|
reasoning: '旧推理',
|
|
chainId: 'old-chain',
|
|
versionId: 'old-version',
|
|
})
|
|
imageMultiImageSession.setTemporaryVariable('scene', 'obsolete')
|
|
imageMultiImageSession.replaceInputImages([
|
|
{ b64: 'AAAA', mimeType: 'image/png' },
|
|
{ b64: 'BBBB', mimeType: 'image/jpeg' },
|
|
])
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([makeDummyRecord()])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-MULTI-001',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'image-multiimage' },
|
|
prompt: { format: 'text', text: '请用图1和图2融合出一张新海报' },
|
|
variables: [
|
|
{ name: 'subject', defaultValue: '图1中的人物' },
|
|
{ name: 'style', defaultValue: '图2的色调' },
|
|
],
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
},
|
|
)
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
imageMultiImageSession,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(currentRoute.value.path).toBe('/image/multiimage')
|
|
expect(imageMultiImageSession.originalPrompt).toBe('请用图1和图2融合出一张新海报')
|
|
expect(imageMultiImageSession.optimizedPrompt).toBe('')
|
|
expect(imageMultiImageSession.reasoning).toBe('')
|
|
expect(imageMultiImageSession.chainId).toBe('')
|
|
expect(imageMultiImageSession.versionId).toBe('')
|
|
expect(imageMultiImageSession.inputImages).toEqual([])
|
|
expect(imageMultiImageSession.temporaryVariables.subject).toBe('图1中的人物')
|
|
expect(imageMultiImageSession.temporaryVariables.style).toBe('图2的色调')
|
|
expect(imageMultiImageSession.temporaryVariables.scene).toBeUndefined()
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('loads multiimage example input images when present', async () => {
|
|
const { pinia } = createTestPinia({
|
|
imageStorageService: {
|
|
getMetadata: async () => null,
|
|
saveImage: async () => {},
|
|
listAllMetadata: async () => [],
|
|
deleteImages: async () => {},
|
|
} as unknown as never,
|
|
})
|
|
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
const imageMultiImageSession = useImageMultiImageSession(pinia)
|
|
|
|
imageMultiImageSession.replaceInputImages([
|
|
{ b64: 'OLD', mimeType: 'image/png' },
|
|
])
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([makeDummyRecord()])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-MULTI-EXAMPLE-001',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const firstImagePath = '/prompt-assets/NB-MULTI-EXAMPLE-001/examples/ex-001/01.png'
|
|
const secondImagePath = '/prompt-assets/NB-MULTI-EXAMPLE-001/examples/ex-001/02.jpg'
|
|
const firstImageUrl = `http://garden.local${firstImagePath}`
|
|
const secondImageUrl = `http://garden.local${secondImagePath}`
|
|
|
|
const v1Payload = {
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'image-multiimage' },
|
|
prompt: { format: 'text', text: '请用图1和图2完成一张融合海报' },
|
|
variables: [],
|
|
assets: {
|
|
examples: [
|
|
{
|
|
id: 'ex-001',
|
|
inputImages: [firstImagePath, secondImagePath],
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async (input) => {
|
|
const url = String(input)
|
|
if (url === 'http://garden.local/api/public/prompt-source/NB-MULTI-EXAMPLE-001') {
|
|
return new Response(JSON.stringify(v1Payload), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
})
|
|
}
|
|
if (url === firstImageUrl) {
|
|
return new Response(new Uint8Array([0, 1, 2, 3]), {
|
|
status: 200,
|
|
headers: { 'content-type': 'image/png' },
|
|
})
|
|
}
|
|
if (url === secondImageUrl) {
|
|
return new Response(new Uint8Array([4, 5, 6, 7]), {
|
|
status: 200,
|
|
headers: { 'content-type': 'image/jpeg' },
|
|
})
|
|
}
|
|
throw new Error(`Unexpected fetch URL: ${url}`)
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
imageMultiImageSession,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(currentRoute.value.path).toBe('/image/multiimage')
|
|
expect(imageMultiImageSession.inputImages).toHaveLength(2)
|
|
expect(imageMultiImageSession.inputImages[0]).toMatchObject({
|
|
b64: 'AAECAw==',
|
|
mimeType: 'image/png',
|
|
})
|
|
expect(imageMultiImageSession.inputImages[1]).toMatchObject({
|
|
b64: 'BAUGBw==',
|
|
mimeType: 'image/jpeg',
|
|
})
|
|
expect(fetchMock).toHaveBeenCalledTimes(3)
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('applies default example parameters into pro-variable temporary variables', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
// Avoid console.warn from useToast (tests fail on console.warn).
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
// Seed existing values; example parameters should override them.
|
|
proVariableSession.setTemporaryVariable('name', 'Bob')
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([makeDummyRecord()])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-PVAR-EX-001',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const v1Payload = {
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'pro-variable' },
|
|
prompt: {
|
|
format: 'text',
|
|
text: 'Hello {{name}}',
|
|
},
|
|
variables: [{ name: 'name' }, { name: 'tone' }],
|
|
assets: {
|
|
examples: [
|
|
{
|
|
id: 'ex-001',
|
|
parameters: {
|
|
name: 'Alice',
|
|
tone: 'friendly',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(JSON.stringify(v1Payload), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
})
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(currentRoute.value.path).toBe('/pro/variable')
|
|
expect(proVariableSession.prompt).toBe('Hello {{name}}')
|
|
|
|
// Example parameters override existing values.
|
|
expect(proVariableSession.getTemporaryVariable('name')).toBe('Alice')
|
|
expect(proVariableSession.getTemporaryVariable('tone')).toBe('friendly')
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('applies selected inline example suffix parameters when provided', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
// Avoid console.warn from useToast (tests fail on console.warn).
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([makeDummyRecord()])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-PVAR-EX-002@ex-b',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const v1Payload = {
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'pro-variable' },
|
|
prompt: {
|
|
format: 'text',
|
|
text: 'Hello {{name}}',
|
|
},
|
|
variables: [{ name: 'name' }],
|
|
assets: {
|
|
examples: [
|
|
{ id: 'ex-a', parameters: { name: 'Alice' } },
|
|
{ id: 'ex-b', parameters: { name: 'Charlie' } },
|
|
],
|
|
},
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(JSON.stringify(v1Payload), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
})
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(currentRoute.value.path).toBe('/pro/variable')
|
|
expect(fetchMock.mock.calls[0]?.[0]).toBe('http://garden.local/api/public/prompt-source/NB-PVAR-EX-002')
|
|
expect(proVariableSession.getTemporaryVariable('name')).toBe('Charlie')
|
|
|
|
// Import params removed from the URL.
|
|
expect(currentRoute.value.query.importCode).toBeUndefined()
|
|
expect(currentRoute.value.query.exampleId).toBeUndefined()
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('injects {{var}} placeholders into image temporary variables', async () => {
|
|
const { pinia } = createTestPinia({
|
|
// Image sessions require ImageStorageService to persist.
|
|
// Provide a minimal stub to avoid console warnings during tests.
|
|
imageStorageService: {
|
|
listAllMetadata: async () => [],
|
|
deleteImages: async () => {},
|
|
} as unknown as never,
|
|
})
|
|
|
|
// Avoid console.warn from useToast (tests fail on console.warn).
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
imageText2ImageSession.updateOptimizedResult({
|
|
optimizedPrompt: 'old-opt',
|
|
reasoning: 'old-r',
|
|
chainId: 'old-chain',
|
|
versionId: 'old-version',
|
|
})
|
|
imageText2ImageSession.testVariantResults = {
|
|
...imageText2ImageSession.testVariantResults,
|
|
a: { result: 'old-a', reasoning: 'old-a-r' },
|
|
b: { result: 'old-b', reasoning: 'old-b-r' },
|
|
}
|
|
|
|
// Existing values should be preserved.
|
|
imageText2ImageSession.setTemporaryVariable('season', 'winter')
|
|
imageText2ImageSession.setTemporaryVariable('obsolete', 'should-delete')
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([makeDummyRecord()])
|
|
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-IMG-001',
|
|
subModeKey: 'image-text2image',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'image-text2image' },
|
|
prompt: { format: 'text', text: 'Draw a {{season}} {{style}} landscape' },
|
|
variables: [{ name: 'season' }, { name: 'style' }],
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
}
|
|
)
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
expect(currentRoute.value.path).toBe('/image/text2image')
|
|
|
|
// Prompt imported into image session.
|
|
expect(imageText2ImageSession.originalPrompt).toBe('Draw a {{season}} {{style}} landscape')
|
|
expect(imageText2ImageSession.optimizedPrompt).toBe('')
|
|
expect(imageText2ImageSession.reasoning).toBe('')
|
|
expect(imageText2ImageSession.chainId).toBe('')
|
|
expect(imageText2ImageSession.versionId).toBe('')
|
|
expect(imageText2ImageSession.testVariantResults.a).toBeNull()
|
|
expect(imageText2ImageSession.testVariantResults.b).toBeNull()
|
|
|
|
// Variables are re-seeded from the import payload after clear-content runs.
|
|
expect(imageText2ImageSession.getTemporaryVariable('season')).toBe('')
|
|
|
|
// Missing variable names are injected as empty strings.
|
|
expect(imageText2ImageSession.getTemporaryVariable('style')).toBe('')
|
|
|
|
// Variables not present in the import payload are removed.
|
|
expect(imageText2ImageSession.getTemporaryVariable('obsolete')).toBeUndefined()
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('loads image2image example input image when present', async () => {
|
|
const { pinia } = createTestPinia({
|
|
imageStorageService: {
|
|
getMetadata: async () => null,
|
|
saveImage: async () => {},
|
|
listAllMetadata: async () => [],
|
|
deleteImages: async () => {},
|
|
} as unknown as never,
|
|
})
|
|
|
|
// Avoid console.warn from useToast (tests fail on console.warn).
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
imageImage2ImageSession.updatePrompt('old prompt')
|
|
imageImage2ImageSession.updateOptimizedResult({
|
|
optimizedPrompt: 'old-opt',
|
|
reasoning: 'old-r',
|
|
chainId: 'old-chain',
|
|
versionId: 'old-version',
|
|
})
|
|
imageImage2ImageSession.updateInputImage({
|
|
imageB64: 'old-b64',
|
|
imageId: null,
|
|
mimeType: 'image/png',
|
|
})
|
|
imageImage2ImageSession.testVariantResults = {
|
|
...imageImage2ImageSession.testVariantResults,
|
|
a: { result: 'old-a', reasoning: 'old-a-r' },
|
|
b: { result: 'old-b', reasoning: 'old-b-r' },
|
|
}
|
|
imageImage2ImageSession.setTemporaryVariable('obsolete', 'should-delete')
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([makeDummyRecord()])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-I2I-001',
|
|
subModeKey: 'image-image2image',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const inputAssetPath = '/prompt-assets/NB-I2I-001/examples/ex-001/01.png'
|
|
const inputAssetUrl = `http://garden.local${inputAssetPath}`
|
|
|
|
const v1Payload = {
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'image-image2image' },
|
|
prompt: { format: 'text', text: 'Transform the image' },
|
|
variables: [],
|
|
assets: {
|
|
examples: [
|
|
{
|
|
id: 'ex-001',
|
|
inputImages: [inputAssetPath],
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async (input) => {
|
|
const url = String(input)
|
|
if (url === 'http://garden.local/api/public/prompt-source/NB-I2I-001') {
|
|
return new Response(JSON.stringify(v1Payload), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
})
|
|
}
|
|
if (url === inputAssetUrl) {
|
|
return new Response(new Uint8Array([0, 1, 2, 3]), {
|
|
status: 200,
|
|
headers: { 'content-type': 'image/png' },
|
|
})
|
|
}
|
|
throw new Error(`Unexpected fetch URL: ${url}`)
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(currentRoute.value.path).toBe('/image/image2image')
|
|
expect(imageImage2ImageSession.originalPrompt).toBe('Transform the image')
|
|
expect(imageImage2ImageSession.optimizedPrompt).toBe('')
|
|
expect(imageImage2ImageSession.reasoning).toBe('')
|
|
expect(imageImage2ImageSession.chainId).toBe('')
|
|
expect(imageImage2ImageSession.versionId).toBe('')
|
|
expect(imageImage2ImageSession.testVariantResults.a).toBeNull()
|
|
expect(imageImage2ImageSession.testVariantResults.b).toBeNull()
|
|
expect(imageImage2ImageSession.getTemporaryVariable('obsolete')).toBeUndefined()
|
|
|
|
// [0,1,2,3] -> AAECAw==
|
|
expect(imageImage2ImageSession.inputImageB64).toBe('AAECAw==')
|
|
expect(imageImage2ImageSession.inputImageMime).toBe('image/png')
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('clears temporary variables when importing an empty variable list into pro-variable', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
// Avoid console.warn from useToast (tests fail on console.warn).
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
// Seed non-empty variables; they should be cleared because the import has no variables.
|
|
proVariableSession.setTemporaryVariable('keep', '1')
|
|
proVariableSession.setTemporaryVariable('alsoRemove', '2')
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([makeDummyRecord()])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-PVAR-EMPTY-001',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const v1Payload = {
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'pro-variable' },
|
|
prompt: {
|
|
format: 'text',
|
|
text: 'Hello',
|
|
},
|
|
variables: [],
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(JSON.stringify(v1Payload), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
})
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
expect(currentRoute.value.path).toBe('/pro/variable')
|
|
expect(proVariableSession.prompt).toBe('Hello')
|
|
|
|
expect(proVariableSession.getTemporaryVariable('keep')).toBeUndefined()
|
|
expect(proVariableSession.getTemporaryVariable('alsoRemove')).toBeUndefined()
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('auto-saves imported prompt into favorites when saveToFavorites=1', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-SAVE-001',
|
|
saveToFavorites: '1',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const favoriteManager = {
|
|
getFavorites: vi.fn(async (): Promise<FavoritePrompt[]> => []),
|
|
getCategories: vi.fn(async () => []),
|
|
addCategory: vi.fn(async ({ name }: { name: string }) => `cat-${name}`),
|
|
addFavorite: vi.fn(async (
|
|
_favorite: Omit<FavoritePrompt, 'id' | 'createdAt' | 'updatedAt' | 'useCount'>
|
|
) => 'fav-new'),
|
|
updateFavorite: vi.fn(async (_id: string, _updates: Partial<FavoritePrompt>) => {}),
|
|
}
|
|
|
|
const imageStorageService = {
|
|
getMetadata: vi.fn(async () => null),
|
|
saveImage: vi.fn(async () => 'saved-image-id'),
|
|
} as any
|
|
|
|
const v1Payload = {
|
|
id: 'prompt-001',
|
|
importCode: 'NB-SAVE-001',
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'basic-system' },
|
|
prompt: { format: 'text', text: 'IMPORTED PROMPT' },
|
|
variables: [
|
|
{
|
|
name: 'topic',
|
|
description: 'topic desc',
|
|
type: 'string',
|
|
required: true,
|
|
defaultValue: 'travel',
|
|
source: 'frontmatter',
|
|
},
|
|
],
|
|
assets: {
|
|
cover: {
|
|
url: '/prompt-assets/cover.png',
|
|
},
|
|
showcases: [
|
|
{
|
|
id: 'show-1',
|
|
images: ['/prompt-assets/show-1.png'],
|
|
},
|
|
],
|
|
examples: [
|
|
{
|
|
id: 'ex-1',
|
|
inputImages: ['/prompt-assets/in-1.png'],
|
|
parameters: {
|
|
topic: 'city',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
meta: {
|
|
title: 'Garden Prompt Title',
|
|
description: 'Garden Prompt Description',
|
|
tags: ['travel', 'city'],
|
|
categoryPath: ['图像创作', '海报'],
|
|
},
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(JSON.stringify(v1Payload), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
})
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
getFavoriteManager: () => favoriteManager,
|
|
getFavoriteImageStorageService: () => imageStorageService,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(favoriteManager.getFavorites).toHaveBeenCalledTimes(1)
|
|
expect(favoriteManager.updateFavorite).not.toHaveBeenCalled()
|
|
expect(favoriteManager.addFavorite).toHaveBeenCalledTimes(1)
|
|
|
|
const savedArg = favoriteManager.addFavorite.mock.calls[0]![0] as Record<string, unknown>
|
|
expect(savedArg.content).toBe('IMPORTED PROMPT')
|
|
expect(savedArg.functionMode).toBe('basic')
|
|
expect(savedArg.optimizationMode).toBe('system')
|
|
expect(savedArg.category).toBe('cat-海报')
|
|
expect(savedArg.tags).toEqual(['travel', 'city'])
|
|
expect(favoriteManager.addCategory).toHaveBeenCalledTimes(2)
|
|
expect(favoriteManager.addCategory).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
|
name: '图像创作',
|
|
parentId: undefined,
|
|
}))
|
|
expect(favoriteManager.addCategory).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
|
name: '海报',
|
|
parentId: 'cat-图像创作',
|
|
}))
|
|
|
|
const metadata = savedArg.metadata as Record<string, unknown>
|
|
const snapshot = metadata.gardenSnapshot as Record<string, unknown>
|
|
expect(snapshot.importCode).toBe('NB-SAVE-001')
|
|
expect(snapshot.gardenBaseUrl).toBe('http://garden.local')
|
|
|
|
const media = metadata.media as Record<string, unknown>
|
|
expect(String(media.coverAssetId || '')).toMatch(/^img_/)
|
|
const mediaAssetIds = media.assetIds as unknown[]
|
|
expect(Array.isArray(mediaAssetIds)).toBe(true)
|
|
expect(mediaAssetIds.length).toBeGreaterThan(0)
|
|
|
|
const snapshotAssets = snapshot.assets as Record<string, unknown>
|
|
const cover = snapshotAssets.cover as Record<string, unknown>
|
|
expect(String(cover.assetId || '')).toMatch(/^img_/)
|
|
expect(cover.url).toBeUndefined()
|
|
|
|
const showcases = snapshotAssets.showcases as Array<Record<string, unknown>>
|
|
const showcaseImageAssetIds = showcases[0]?.imageAssetIds as unknown[]
|
|
expect(showcaseImageAssetIds).toHaveLength(1)
|
|
expect(String(showcaseImageAssetIds[0] || '')).toMatch(/^img_/)
|
|
|
|
const examples = snapshotAssets.examples as Array<Record<string, unknown>>
|
|
const inputImageAssetIds = examples[0]?.inputImageAssetIds as unknown[]
|
|
expect(inputImageAssetIds).toHaveLength(1)
|
|
expect(String(inputImageAssetIds[0] || '')).toMatch(/^img_/)
|
|
expect(imageStorageService.saveImage).toHaveBeenCalled()
|
|
|
|
// saveToFavorites 查询参数会和 import 参数一起清理。
|
|
expect(currentRoute.value.query.importCode).toBeUndefined()
|
|
expect(currentRoute.value.query.saveToFavorites).toBeUndefined()
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('opens save-favorite dialog when saveToFavorites=confirm', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
const loadingDestroy = vi.fn()
|
|
const createReactive = (destroy = vi.fn()): MessageReactive => ({
|
|
destroy,
|
|
} as unknown as MessageReactive)
|
|
const messageApi = {
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive(loadingDestroy)),
|
|
}
|
|
setGlobalMessageApi(messageApi)
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
basicSystemSession.updatePrompt('KEEP WORKSPACE')
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-CONFIRM-001',
|
|
saveToFavorites: 'confirm',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const favoriteManager = {
|
|
getFavorites: vi.fn(async (): Promise<FavoritePrompt[]> => []),
|
|
getCategories: vi.fn(async () => [
|
|
{
|
|
id: 'cat-text-root',
|
|
name: '文本生成',
|
|
parentId: undefined,
|
|
createdAt: Date.now(),
|
|
sortOrder: 0,
|
|
},
|
|
{
|
|
id: 'cat-marketing-leaf',
|
|
name: '营销文案',
|
|
parentId: 'cat-text-root',
|
|
createdAt: Date.now(),
|
|
sortOrder: 0,
|
|
},
|
|
]),
|
|
addCategory: vi.fn(async ({ name }: { name: string }) => `cat-${name}`),
|
|
addFavorite: vi.fn(async (
|
|
_favorite: Omit<FavoritePrompt, 'id' | 'createdAt' | 'updatedAt' | 'useCount'>
|
|
) => 'fav-new'),
|
|
updateFavorite: vi.fn(async (_id: string, _updates: Partial<FavoritePrompt>) => {}),
|
|
}
|
|
|
|
const openSaveFavoriteDialog = vi.fn()
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
id: 'prompt-confirm-1',
|
|
importCode: 'NB-CONFIRM-001',
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'basic-system' },
|
|
prompt: { format: 'text', text: 'CONFIRM CONTENT' },
|
|
variables: [],
|
|
meta: {
|
|
title: 'Confirm Prompt Title',
|
|
description: 'Confirm Prompt Description',
|
|
tags: ['confirm', 'garden'],
|
|
categoryPath: ['文本生成', '营销文案'],
|
|
categoryKey: '文本生成',
|
|
},
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
}
|
|
)
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
getFavoriteManager: () => favoriteManager,
|
|
openSaveFavoriteDialog,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(openSaveFavoriteDialog).toHaveBeenCalledTimes(1)
|
|
expect(messageApi.info).toHaveBeenCalledWith(
|
|
String(i18n.global.t('common.promptGarden.importingStatus')),
|
|
expect.objectContaining({
|
|
duration: 0,
|
|
closable: false,
|
|
})
|
|
)
|
|
expect(loadingDestroy).toHaveBeenCalledTimes(1)
|
|
expect(basicSystemSession.prompt).toBe('KEEP WORKSPACE')
|
|
const savedArg = openSaveFavoriteDialog.mock.calls[0]?.[0] as {
|
|
content: string
|
|
prefill?: {
|
|
title?: string
|
|
description?: string
|
|
tags?: string[]
|
|
category?: string
|
|
functionMode?: string
|
|
metadata?: Record<string, unknown>
|
|
}
|
|
}
|
|
|
|
expect(savedArg.content).toBe('CONFIRM CONTENT')
|
|
expect(savedArg.prefill?.title).toBe('Confirm Prompt Title')
|
|
expect(savedArg.prefill?.description).toBe('Confirm Prompt Description')
|
|
expect(savedArg.prefill?.tags).toEqual(['confirm', 'garden'])
|
|
expect(savedArg.prefill?.category).toBe('cat-marketing-leaf')
|
|
expect(savedArg.prefill?.functionMode).toBe('basic')
|
|
expect(savedArg.prefill?.metadata?.gardenSnapshot).toBeTruthy()
|
|
|
|
expect(favoriteManager.getFavorites).not.toHaveBeenCalled()
|
|
expect(favoriteManager.getCategories).toHaveBeenCalledTimes(1)
|
|
expect(favoriteManager.addFavorite).not.toHaveBeenCalled()
|
|
expect(favoriteManager.updateFavorite).not.toHaveBeenCalled()
|
|
|
|
expect(currentRoute.value.query.importCode).toBeUndefined()
|
|
expect(currentRoute.value.query.saveToFavorites).toBeUndefined()
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('auto-saves to favorites with URL media fallback when snapshot image persistence fails', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
basicSystemSession.updatePrompt('KEEP WORKSPACE')
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-SAVE-FAIL-001',
|
|
saveToFavorites: '1',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const favoriteManager = {
|
|
getFavorites: vi.fn(async (): Promise<FavoritePrompt[]> => []),
|
|
getCategories: vi.fn(async () => []),
|
|
addCategory: vi.fn(async ({ name }: { name: string }) => `cat-${name}`),
|
|
addFavorite: vi.fn(async (
|
|
_favorite: Omit<FavoritePrompt, 'id' | 'createdAt' | 'updatedAt' | 'useCount'>
|
|
) => 'fav-new'),
|
|
updateFavorite: vi.fn(async (_id: string, _updates: Partial<FavoritePrompt>) => {}),
|
|
}
|
|
|
|
const imageStorageService = {
|
|
getMetadata: vi.fn(async () => null),
|
|
saveImage: vi.fn(async () => {
|
|
throw new Error('boom')
|
|
}),
|
|
} as any
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
id: 'prompt-asset-fail-1',
|
|
importCode: 'NB-SAVE-FAIL-001',
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'basic-system' },
|
|
prompt: { format: 'text', text: 'PROMPT WITH ASSET FAILURE' },
|
|
variables: [],
|
|
assets: {
|
|
cover: {
|
|
url: '/prompt-assets/cover.png',
|
|
},
|
|
showcases: [
|
|
{
|
|
id: 'show-1',
|
|
images: ['/prompt-assets/show-1.png'],
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
}
|
|
)
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
getFavoriteManager: () => favoriteManager,
|
|
getFavoriteImageStorageService: () => imageStorageService,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(favoriteManager.getFavorites).toHaveBeenCalledTimes(1)
|
|
expect(basicSystemSession.prompt).toBe('KEEP WORKSPACE')
|
|
expect(favoriteManager.addFavorite).toHaveBeenCalledTimes(1)
|
|
expect(favoriteManager.updateFavorite).not.toHaveBeenCalled()
|
|
const favoriteArg = favoriteManager.addFavorite.mock.calls[0]?.[0]
|
|
expect(favoriteArg?.metadata?.media).toEqual({
|
|
coverAssetId: undefined,
|
|
coverUrl: 'http://garden.local/prompt-assets/cover.png',
|
|
assetIds: [],
|
|
urls: ['http://garden.local/prompt-assets/show-1.png'],
|
|
})
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('opens save-favorite dialog with URL media fallback when snapshot image persistence fails', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-CONFIRM-FAIL-001',
|
|
saveToFavorites: 'confirm',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const openSaveFavoriteDialog = vi.fn()
|
|
const imageStorageService = {
|
|
getMetadata: vi.fn(async () => null),
|
|
saveImage: vi.fn(async () => {
|
|
throw new Error('boom')
|
|
}),
|
|
} as any
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
id: 'prompt-confirm-fail-1',
|
|
importCode: 'NB-CONFIRM-FAIL-001',
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'basic-system' },
|
|
prompt: { format: 'text', text: 'CONFIRM CONTENT WITH IMAGE' },
|
|
variables: [],
|
|
assets: {
|
|
cover: {
|
|
url: '/prompt-assets/cover.png',
|
|
},
|
|
showcases: [
|
|
{
|
|
id: 'show-1',
|
|
images: ['/prompt-assets/show-1.png'],
|
|
},
|
|
],
|
|
},
|
|
meta: {
|
|
title: 'Confirm Prompt Title',
|
|
},
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
}
|
|
)
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
getFavoriteImageStorageService: () => imageStorageService,
|
|
openSaveFavoriteDialog,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(openSaveFavoriteDialog).toHaveBeenCalledTimes(1)
|
|
const savedArg = openSaveFavoriteDialog.mock.calls[0]?.[0] as {
|
|
prefill?: {
|
|
metadata?: Record<string, unknown>
|
|
}
|
|
}
|
|
|
|
expect(savedArg.prefill?.metadata?.media).toEqual({
|
|
coverAssetId: undefined,
|
|
coverUrl: 'http://garden.local/prompt-assets/cover.png',
|
|
assetIds: [],
|
|
urls: ['http://garden.local/prompt-assets/show-1.png'],
|
|
})
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('does not auto-save when saveToFavorites flag is absent', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-NO-SAVE-001',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const favoriteManager = {
|
|
getFavorites: vi.fn(async (): Promise<FavoritePrompt[]> => []),
|
|
getCategories: vi.fn(async () => []),
|
|
addCategory: vi.fn(async ({ name }: { name: string }) => `cat-${name}`),
|
|
addFavorite: vi.fn(async (
|
|
_favorite: Omit<FavoritePrompt, 'id' | 'createdAt' | 'updatedAt' | 'useCount'>
|
|
) => 'fav-new'),
|
|
updateFavorite: vi.fn(async (_id: string, _updates: Partial<FavoritePrompt>) => {}),
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'basic-system' },
|
|
prompt: { format: 'text', text: 'NO SAVE PROMPT' },
|
|
variables: [],
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
}
|
|
)
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
getFavoriteManager: () => favoriteManager,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(favoriteManager.getFavorites).not.toHaveBeenCalled()
|
|
expect(favoriteManager.addFavorite).not.toHaveBeenCalled()
|
|
expect(favoriteManager.updateFavorite).not.toHaveBeenCalled()
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('upserts existing favorite by gardenSnapshot importCode + gardenBaseUrl', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
setGlobalMessageApi({
|
|
success: vi.fn(() => createReactive()),
|
|
error: vi.fn(() => createReactive()),
|
|
warning: vi.fn(() => createReactive()),
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-SAVE-UPSERT-001',
|
|
saveToFavorites: 'true',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/pro/multi', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const existingFavorite: FavoritePrompt = {
|
|
id: 'fav-existing',
|
|
title: 'old',
|
|
content: 'old',
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
tags: [],
|
|
useCount: 0,
|
|
functionMode: 'basic',
|
|
optimizationMode: 'system',
|
|
metadata: {
|
|
gardenSnapshot: {
|
|
importCode: 'NB-SAVE-UPSERT-001',
|
|
gardenBaseUrl: 'http://garden.local',
|
|
},
|
|
},
|
|
}
|
|
|
|
const favoriteManager = {
|
|
getFavorites: vi.fn(async (): Promise<FavoritePrompt[]> => [
|
|
existingFavorite,
|
|
]),
|
|
getCategories: vi.fn(async () => []),
|
|
addCategory: vi.fn(async ({ name }: { name: string }) => `cat-${name}`),
|
|
addFavorite: vi.fn(async (
|
|
_favorite: Omit<FavoritePrompt, 'id' | 'createdAt' | 'updatedAt' | 'useCount'>
|
|
) => 'fav-new'),
|
|
updateFavorite: vi.fn(async (_id: string, _updates: Partial<FavoritePrompt>) => {}),
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'pro-multi' },
|
|
prompt: {
|
|
format: 'messages',
|
|
messages: [
|
|
{
|
|
id: 'm1',
|
|
role: 'system',
|
|
content: 'System content',
|
|
},
|
|
{
|
|
id: 'm2',
|
|
role: 'user',
|
|
content: 'User content',
|
|
},
|
|
],
|
|
},
|
|
variables: [],
|
|
meta: {
|
|
title: 'new title',
|
|
},
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
}
|
|
)
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
getFavoriteManager: () => favoriteManager,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(favoriteManager.getFavorites).toHaveBeenCalledTimes(1)
|
|
expect(favoriteManager.addFavorite).not.toHaveBeenCalled()
|
|
expect(favoriteManager.updateFavorite).toHaveBeenCalledTimes(1)
|
|
expect(favoriteManager.updateFavorite).toHaveBeenCalledWith(
|
|
'fav-existing',
|
|
expect.objectContaining({
|
|
content: '[system] System content\n\n[user] User content',
|
|
functionMode: 'context',
|
|
optimizationMode: 'system',
|
|
metadata: expect.objectContaining({
|
|
gardenSnapshot: expect.objectContaining({
|
|
importCode: 'NB-SAVE-UPSERT-001',
|
|
gardenBaseUrl: 'http://garden.local',
|
|
}),
|
|
}),
|
|
})
|
|
)
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
|
|
it('shows a warning when Prompt Garden auto-save to favorites fails', async () => {
|
|
const { pinia } = createTestPinia()
|
|
|
|
const createReactive = (): MessageReactive => ({
|
|
destroy: () => {},
|
|
} as unknown as MessageReactive)
|
|
const successMock = vi.fn(() => createReactive())
|
|
const errorMock = vi.fn(() => createReactive())
|
|
const warningMock = vi.fn(() => createReactive())
|
|
setGlobalMessageApi({
|
|
success: successMock,
|
|
error: errorMock,
|
|
warning: warningMock,
|
|
info: vi.fn(() => createReactive()),
|
|
})
|
|
|
|
const basicSystemSession = useBasicSystemSession(pinia)
|
|
const basicUserSession = useBasicUserSession(pinia)
|
|
const proMultiMessageSession = useProMultiMessageSession(pinia)
|
|
const proVariableSession = useProVariableSession(pinia)
|
|
const imageText2ImageSession = useImageText2ImageSession(pinia)
|
|
const imageImage2ImageSession = useImageImage2ImageSession(pinia)
|
|
|
|
const optimizerCurrentVersions = ref<PromptRecordChain['versions']>([])
|
|
const hasRestoredInitialState = ref(false)
|
|
const isLoadingExternalData = ref(false)
|
|
|
|
const query: LocationQuery = {
|
|
importCode: 'NB-AUTO-SAVE-FAIL-001',
|
|
saveToFavorites: 'true',
|
|
}
|
|
|
|
const currentRoute = ref<RouteLocationNormalizedLoaded>(makeRoute('/basic/system', query))
|
|
|
|
let replaceResolve: (() => void) | undefined
|
|
const replaceDone = new Promise<void>((resolve) => {
|
|
replaceResolve = resolve
|
|
})
|
|
|
|
const push: Router['push'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
return undefined
|
|
})
|
|
|
|
const replace: Router['replace'] = vi.fn(async (to) => {
|
|
applyNavigation(currentRoute, to)
|
|
replaceResolve?.()
|
|
return undefined
|
|
})
|
|
|
|
const router: Pick<Router, 'currentRoute' | 'push' | 'replace'> = {
|
|
currentRoute,
|
|
push,
|
|
replace,
|
|
}
|
|
|
|
const favoriteManager = {
|
|
getFavorites: vi.fn(async (): Promise<FavoritePrompt[]> => []),
|
|
getCategories: vi.fn(async () => []),
|
|
addCategory: vi.fn(async ({ name }: { name: string }) => `cat-${name}`),
|
|
addFavorite: vi.fn(async () => {
|
|
throw new Error('favorites payload exceeds hard limit')
|
|
}),
|
|
updateFavorite: vi.fn(async (_id: string, _updates: Partial<FavoritePrompt>) => {}),
|
|
}
|
|
|
|
const fetchMock = vi.fn<
|
|
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
>(async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
schema: 'prompt-garden.prompt.v1',
|
|
schemaVersion: 1,
|
|
optimizerTarget: { subModeKey: 'basic-system' },
|
|
prompt: { format: 'text', text: 'AUTO SAVE FAIL PROMPT' },
|
|
variables: [],
|
|
meta: {
|
|
title: 'Auto Save Fail Prompt',
|
|
},
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
},
|
|
)
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const scope = effectScope()
|
|
try {
|
|
scope.run(() => {
|
|
useAppPromptGardenImport({
|
|
router,
|
|
hasRestoredInitialState,
|
|
isLoadingExternalData,
|
|
gardenBaseUrl: 'http://garden.local',
|
|
basicSystemSession,
|
|
basicUserSession,
|
|
proMultiMessageSession,
|
|
proVariableSession,
|
|
imageText2ImageSession,
|
|
imageImage2ImageSession,
|
|
getFavoriteManager: () => favoriteManager,
|
|
optimizerCurrentVersions,
|
|
})
|
|
})
|
|
|
|
hasRestoredInitialState.value = true
|
|
|
|
await replaceDone
|
|
await waitForCondition(() => isLoadingExternalData.value === false)
|
|
|
|
expect(favoriteManager.getFavorites).toHaveBeenCalledTimes(1)
|
|
expect(favoriteManager.addFavorite).toHaveBeenCalledTimes(1)
|
|
expect(warningMock).toHaveBeenCalledTimes(1)
|
|
expect(warningMock.mock.calls[0]?.[0]).toBe(
|
|
String(i18n.global.t('toast.warning.promptGardenFavoriteSaveFailed')),
|
|
)
|
|
expect(errorMock).not.toHaveBeenCalled()
|
|
expect(successMock).toHaveBeenCalled()
|
|
} finally {
|
|
scope.stop()
|
|
}
|
|
})
|
|
})
|