Files
swarm-assistent/test/patch.test.js
T

268 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});