import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { extractPatch, isPatchObject, normalizePatch, setPatchKeys, } 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('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('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('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); }); });