import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { extractPatch, isPatchObject, liveStreamProse, normalizePatch, setPatchKeys, stripFenceArtifacts, visibleProse, } from '../src/patch.js'; import { mergeDelta, emptySession, patchWantsGenerate, sessionFromLegacyParams, toPersistParams, resolveTurnIntent, mergeExactParamsForGenerate, resolveExactProfileDefaults, } from '../src/session.js'; describe('patch.js', () => { it('extractPatch finds generation patch in fence', () => { const text = 'Here you go\n```json\n{"prompt":"A red fox in snow","generate":true}\n```'; const { prose, patch } = extractPatch(text); assert.ok(patch); assert.equal(patch.prompt, 'A red fox in snow'); assert.equal(patch.generate, true); assert.ok(!prose.includes('```')); }); it('normalizePatch maps legacy actions generate', () => { const p = normalizePatch({ prompt: 'x', actions: ['generate'] }); assert.equal(p.generate, true); }); it('extractPatch prefers last terminal fence over a later weak pack fence', () => { const text = [ 'ok', '```json', '{"prompt":"A slender redhead in a leather jacket, 85mm, street light","generate":true}', '```', '```json', '{"pack":"ordinary"}', '```', ].join('\n'); const { patch } = extractPatch(text); assert.equal(patch.generate, true); assert.match(patch.prompt, /redhead/); }); it('extractPatch accepts generate true string and unfenced object', () => { const fenced = extractPatch('ok\n```json\n{"prompt":"A slender redhead in a leather jacket, 85mm","generate":"true"}\n```'); assert.equal(fenced.patch.generate, true); assert.match(fenced.patch.prompt, /redhead/); const raw = extractPatch('done\n{"prompt":"A fiery redhead in leather, street light","generate":true}'); assert.equal(raw.patch.generate, true); assert.equal(raw.prose, 'done'); }); it('visibleProse hides the JSON fence when chat text is empty', () => { assert.equal(visibleProse('```json\n{"prompt":"fox","generate":true}\n```'), ''); assert.equal(visibleProse('Ок, держи\n```json\n{"prompt":"fox"}\n```'), 'Ок, держи'); assert.equal(visibleProse('просто чат без патча'), 'просто чат без патча'); }); it('extractPatch strips orphan ```json when JSON is unfenced', () => { const text = 'О, круто\n\n```json\n{"prompt":"girl on leather sofa, flirt","generate":true}'; const { prose, patch } = extractPatch(text); assert.ok(patch); assert.equal(patch.generate, true); assert.equal(prose, 'О, круто'); assert.ok(!prose.includes('```')); }); it('liveStreamProse hides unclosed fence while streaming', () => { const partial = 'О, круто\n\n```json\n{"prompt":"girl'; assert.equal(liveStreamProse(partial), 'О, круто'); assert.equal( liveStreamProse('Ок\n```json\n{"prompt":"fox","generate":true}\n```'), 'Ок', ); }); it('stripFenceArtifacts removes lone JSON Patch headers', () => { assert.equal(stripFenceArtifacts('### JSON Patch\n'), ''); }); it('scheduler-only patch is detected with full key list', () => { setPatchKeys(['prompt', 'scheduler', 'generate', 'ask']); assert.equal(isPatchObject({ scheduler: 'euler' }), true); }); }); describe('session.js', () => { it('mergeDelta is sparse and keeps other fields', () => { let s = emptySession(); s.gen.prompt = 'old'; s.gen.steps = 8; s = mergeDelta(s, { aspect: '16:9', generate: true }); assert.equal(s.gen.prompt, 'old'); assert.equal(s.gen.steps, 8); assert.equal(s.gen.aspect, '16:9'); assert.equal(patchWantsGenerate({ generate: true }), true); assert.equal(patchWantsGenerate({ actions: ['generate'] }), true); }); it('legacy flat params upgrade to session shape', () => { const s = sessionFromLegacyParams({ prompt: 'fox', steps: 28, loras: [{ name: 'a', weight: 0.8 }], genResults: [{ id: 'var1', src: '/View/x.png' }], persona: 'neutral', }); assert.equal(s.gen.prompt, 'fox'); assert.equal(s.gen.steps, 28); assert.equal(s.board.genResults.length, 1); const blob = toPersistParams(s); assert.ok(blob.gen); assert.ok(blob.board); }); it('resolveTurnIntent vetoes generate', () => { const intent = resolveTurnIntent( { generate: true, prompt: 'x' }, 'не генерируй', { vetoFn: (t) => /не\s+генерир/i.test(t) }, ); assert.equal(intent.generate, false); assert.equal(intent.vetoed, true); }); it('resolveTurnIntent honors сгенерируй when prompt is present', () => { const intent = resolveTurnIntent( { prompt: 'A slender redhead in a leather jacket' }, 'сгенерируй', { askGenerateFn: (t) => /сгенер/i.test(t) }, ); assert.equal(intent.generate, true); assert.equal(patchWantsGenerate({ generate: 'true' }), true); assert.equal(patchWantsGenerate({ generate: true }), true); }); it('resolveTurnIntent ignores model generate:true on a knowledge question', () => { const intent = resolveTurnIntent( { prompt: 'civitai example prompt', generate: true }, 'а ты знаешь какие то промпты с civitai', { askGenerateFn: (t) => /сгенер|нарисуй/i.test(t) }, ); assert.equal(intent.generate, false); assert.equal(intent.modelAsked, true); }); it('mergeExactParamsForGenerate fills turbo profile when LLM omits steps/cfg', () => { const exact = { generation: { profile: 'turbo', steps: 8, cfg: 1, sigma_shift: 1.15 }, profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 }, raw: { steps: 28, cfg: 4.5, sigma_shift: 1.15 }, }, }; const { patch, clearSessionKeys, profile } = mergeExactParamsForGenerate( { prompt: 'a fox', generate: true }, { exact, profiles: exact.profiles, profileName: 'turbo', sessionExact: { steps: 20, cfg: 7 }, userParamIntent: false, }, ); assert.equal(profile, 'turbo'); assert.equal(patch.steps, 8); assert.equal(patch.cfg, 1); assert.equal(patch.sigma_shift, 1.15); assert.deepEqual(clearSessionKeys.sort(), ['cfg', 'steps']); }); it('mergeExactParamsForGenerate keeps user-intent sessionExact and explicit patch', () => { const exact = { generation: { steps: 8, cfg: 1 }, profiles: { raw: { steps: 28, cfg: 4.5, sigma_shift: 1.15 } }, }; const kept = mergeExactParamsForGenerate( { generate: true, actions: ['generate'] }, { exact, profiles: exact.profiles, profileName: 'raw', sessionExact: { steps: 20 }, userParamIntent: true, }, ); assert.equal(kept.patch.steps, 20); assert.equal(kept.patch.cfg, 4.5); assert.deepEqual(kept.clearSessionKeys, []); const explicit = mergeExactParamsForGenerate( { generate: true, steps: 12, cfg: 2 }, { exact, profiles: exact.profiles, profileName: 'turbo', userParamIntent: true }, ); assert.equal(explicit.patch.steps, 12); assert.equal(explicit.patch.cfg, 2); const forced = mergeExactParamsForGenerate( { generate: true, steps: 20, cfg: 7 }, { exact: { generation: { steps: 8, cfg: 1 }, profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } } }, profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } }, profileName: 'turbo', userParamIntent: false, }, ); assert.equal(forced.patch.steps, 8); assert.equal(forced.patch.cfg, 1); }); it('mergeExactParamsForGenerate preserves pinned session_exact sampler/scheduler when LLM omits them', () => { const exact = { generation: { profile: 'turbo', steps: 8, cfg: 1, sampler: 'euler', scheduler: 'normal' }, profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } }, }; const { patch, clearSessionKeys } = mergeExactParamsForGenerate( { prompt: 'a fox', generate: true }, { exact, profiles: exact.profiles, profileName: 'turbo', sessionExact: { sampler: 'heun', scheduler: 'simple', aspect: '16:9' }, userParamIntent: false, }, ); assert.equal(patch.sampler, 'heun'); assert.equal(patch.scheduler, 'simple'); assert.equal(patch.aspect, '16:9'); assert.equal(patch.steps, 8); assert.deepEqual(clearSessionKeys.sort(), []); }); it('mergeExactParamsForGenerate does not overwrite explicit LLM sampler/scheduler', () => { const { patch } = mergeExactParamsForGenerate( { generate: true, sampler: 'dpmpp_2m', scheduler: 'karras' }, { exact: { generation: { steps: 8, cfg: 1 } }, profiles: { turbo: { steps: 8, cfg: 1 } }, profileName: 'turbo', sessionExact: { sampler: 'heun', scheduler: 'simple' }, userParamIntent: false, }, ); assert.equal(patch.sampler, 'dpmpp_2m'); assert.equal(patch.scheduler, 'karras'); }); it('resolveExactProfileDefaults picks raw over generation defaults', () => { const d = resolveExactProfileDefaults({ exact: { generation: { steps: 8, cfg: 1, sigma_shift: 1.15 } }, profiles: { raw: { steps: 28, cfg: 4.5 } }, profileName: 'raw', }); assert.equal(d.steps, 28); assert.equal(d.cfg, 4.5); assert.equal(d.sigma_shift, 1.15); }); });