import test from 'node:test' import assert from 'node:assert/strict' import { EventEmitter } from 'node:events' import { PassThrough } from 'node:stream' import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import ts from 'typescript' import { createYueGpHost, validateYueGpRequest } from '../scripts/yuegp-host.mjs' const request = { id: 'test-song-123456', tags: 'pop, warm vocals', lyrics: '[Verse 1]\nA quiet morning, a new day', seed: 0 } test('YuEGP defaults are profile 1, 60 seconds, and no silent fallback', () => { assert.deepEqual(validateYueGpRequest(request), { ...request, profile: 1, duration: 60 }) assert.equal(validateYueGpRequest({ ...request, profile: 3 }).profile, 3) for (const profile of [0, 2, 4, 5, '1']) assert.throws(() => validateYueGpRequest({ ...request, profile })) assert.throws(() => validateYueGpRequest({ ...request, lyrics: '[Verse]\nA\n[Chorus]\nB' })) assert.throws(() => validateYueGpRequest({ ...request, id: '../escape' })) }) function fixture() { const root = mkdtempSync(join(tmpdir(), 'aigen-yuegp-')) const python = join(root, 'python.exe') writeFileSync(python, '') writeFileSync(join(root, 'aigen-ready.json'), '{}') const child = new EventEmitter() child.stdout = new PassThrough(); child.stderr = new PassThrough() child.kill = () => { setImmediate(() => child.emit('close', 1)); return true } const calls = [] const dataDir = join(root, 'jobs') const host = createYueGpHost({ root, python, dataDir, prepare: async () => {}, leaseValid: () => true, spawnProcess: (...args) => { calls.push(args); return child } }) return { host, child, calls, root, dataDir } } test('host launches only the standalone worker, preserves seed 0, and deduplicates POST retries', async () => { const f = fixture() await f.host.start(request, 'lease') await f.host.start(request, 'lease') assert.equal(f.calls.length, 1) const [, args, opts] = f.calls[0] assert.equal(args.at(-1), '1') assert.equal(args.includes('--compile'), false) assert.equal(opts.windowsHide, true) assert.equal(opts.shell, false) const posted = JSON.parse(readFileSync(join(f.dataDir, request.id, 'request.json'))) assert.equal(posted.seed, 0) assert.equal(posted.stage1Model, 'm-a-p/YuE-s1-7B-anneal-en-cot') assert.equal(posted.stage2Model, 'm-a-p/YuE-s2-1B-general') await assert.rejects(f.host.start({ ...request, id: 'second-song-12345' }, 'lease'), /already running/) await f.host.cancel(request.id) assert.equal(f.host.busy(), false) assert.equal(f.host.read(request.id).status, 'cancelled') }) test('partial log chunks report progress, and audio is unavailable until process exit', async () => { const f = fixture() await f.host.start(request, 'lease') f.child.stdout.write('AIGEN_EVENT {"stage":"stage1",') f.child.stdout.write('"step":100,"maxStep":6000,"progress":1.7}\n') assert.equal(f.host.read(request.id).step, 100) writeFileSync(join(f.dataDir, request.id, 'audio.wav'), 'audio') assert.equal(f.host.audio(request.id), null) f.child.emit('close', 0) assert.equal(f.host.read(request.id).status, 'complete') assert.equal(f.host.busy(), false) assert.ok(f.host.audio(request.id).endsWith('audio.wav')) }) test('worker failure is durable and never launches an alternate profile', async () => { const f = fixture() await f.host.start(request, 'lease') f.child.stdout.write('AIGEN_EVENT {"stage":"error","error":"CUDA out of memory"}\n') f.child.emit('close', 1) assert.match(f.host.read(request.id).error, /out of memory/) assert.equal(f.calls.length, 1) assert.equal(f.host.read(request.id).profile, 1) }) test('the Comfy workflow builder refuses YuE while Ace graphs still build', () => { const source = readFileSync(new URL('../server/utils/musicWorkflow.ts', import.meta.url), 'utf8') const code = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText const exports = {} new Function('require', 'exports', 'useRuntimeConfig', code)(id => { if (id.endsWith('.json')) return { default: JSON.parse(readFileSync(new URL(`../server/assets/${id.split('/').pop()}`, import.meta.url))) } if (id === '~/utils/music') return { parseMusicBpm: () => 120 } throw new Error(id) }, exports, () => ({})) const params = { tags: 'pop', lyrics: '[Verse]\nhello', duration: 60, steps: 8, seed: 0, cfg: 1, lyricsStrength: 0.9 } assert.throws(() => exports.buildMusicWorkflow({ ...params, engine: 'yue' }), /standalone YuEGP/) for (const engine of ['ace-step', 'ace-step-1.5']) { const graph = exports.buildMusicWorkflow({ ...params, engine }) assert.ok(Object.values(graph).some(n => n.class_type === 'KSampler' && n.inputs.seed === 0)) } assert.equal(existsSync(new URL('../server/assets/workflow_yue.json', import.meta.url)), false) })