init
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { fixAgnesImageSize, isAgnesImageConfig } = require('../src/services/imageClient');
|
||||
|
||||
describe('fixAgnesImageSize', () => {
|
||||
it('maps 9:16 project size to Agnes portrait preset', () => {
|
||||
assert.equal(fixAgnesImageSize('1440x2560'), '1024x1792');
|
||||
});
|
||||
|
||||
it('maps 16:9 project size to Agnes landscape preset', () => {
|
||||
assert.equal(fixAgnesImageSize('2560x1440'), '1792x1024');
|
||||
});
|
||||
|
||||
it('maps 1:1 project size to Agnes square preset', () => {
|
||||
assert.equal(fixAgnesImageSize('1920x1920'), '1024x1024');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAgnesImageConfig', () => {
|
||||
it('detects agnes provider even when api_protocol is openai', () => {
|
||||
assert.equal(
|
||||
isAgnesImageConfig({ provider: 'agnes', base_url: 'https://apihub.agnes-ai.com/v1', api_protocol: 'openai' }, 'agnes-image-2.1-flash'),
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { buildAgnesVideoImagePayload, formatVideoPostBodyForLog } = require('../src/services/videoClient');
|
||||
|
||||
describe('formatVideoPostBodyForLog', () => {
|
||||
it('keeps full http URLs and labels extra_body images with index', () => {
|
||||
const formatted = formatVideoPostBodyForLog({
|
||||
model: 'agnes-video-v2.0',
|
||||
prompt: 'test prompt',
|
||||
extra_body: {
|
||||
image: ['https://cdn/a.jpg', 'https://cdn/b.png'],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(formatted.extra_body.image, [
|
||||
'[0] https://cdn/a.jpg',
|
||||
'[1] https://cdn/b.png',
|
||||
]);
|
||||
assert.equal(formatted.prompt, 'test prompt');
|
||||
});
|
||||
|
||||
it('summarizes base64 image fields', () => {
|
||||
const dataUrl = 'data:image/png;base64,' + 'A'.repeat(100);
|
||||
const formatted = formatVideoPostBodyForLog({ image: dataUrl });
|
||||
assert.match(formatted.image, /^\(base64, \d+ chars\)$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildAgnesVideoImagePayload', () => {
|
||||
it('uses extra_body.image array for omni multi-reference without keyframes mode', () => {
|
||||
const refs = ['https://cdn/a.jpg', 'https://cdn/b.png', 'https://cdn/c.png'];
|
||||
const out = buildAgnesVideoImagePayload({
|
||||
useOmniReference: true,
|
||||
resolvedRefs: refs,
|
||||
firstResolved: 'https://cdn/a.jpg',
|
||||
lastResolved: 'https://cdn/z.jpg',
|
||||
});
|
||||
assert.equal(out.strategy, 'omni_reference_extra_body');
|
||||
assert.deepEqual(out.extra_body, { image: refs });
|
||||
assert.equal(out.image, undefined);
|
||||
assert.equal(out.extra_body.mode, undefined);
|
||||
});
|
||||
|
||||
it('uses single top-level image string for one omni reference', () => {
|
||||
const out = buildAgnesVideoImagePayload({
|
||||
useOmniReference: true,
|
||||
resolvedRefs: ['https://cdn/scene.jpg'],
|
||||
firstResolved: null,
|
||||
lastResolved: null,
|
||||
});
|
||||
assert.equal(out.strategy, 'omni_reference_single');
|
||||
assert.equal(out.image, 'https://cdn/scene.jpg');
|
||||
});
|
||||
|
||||
it('uses extra_body keyframes only for classic first/last (not omni)', () => {
|
||||
const out = buildAgnesVideoImagePayload({
|
||||
useOmniReference: false,
|
||||
resolvedRefs: [],
|
||||
firstResolved: 'https://cdn/first.jpg',
|
||||
lastResolved: 'https://cdn/last.jpg',
|
||||
});
|
||||
assert.equal(out.strategy, 'classic_keyframes');
|
||||
assert.deepEqual(out.extra_body, {
|
||||
mode: 'keyframes',
|
||||
image: ['https://cdn/first.jpg', 'https://cdn/last.jpg'],
|
||||
});
|
||||
assert.equal(out.image, undefined);
|
||||
});
|
||||
|
||||
it('does not use keyframes mode when omni refs exist', () => {
|
||||
const refs = ['https://cdn/s.jpg', 'https://cdn/c.jpg'];
|
||||
const out = buildAgnesVideoImagePayload({
|
||||
useOmniReference: true,
|
||||
resolvedRefs: refs,
|
||||
firstResolved: 'https://cdn/s.jpg',
|
||||
lastResolved: 'https://cdn/l.jpg',
|
||||
});
|
||||
assert.equal(out.strategy, 'omni_reference_extra_body');
|
||||
assert.equal(out.extra_body.mode, undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { pickProxyVideoUrl } = require('../src/services/videoClient');
|
||||
|
||||
describe('pickProxyVideoUrl Agnes completed task', () => {
|
||||
it('reads MP4 from remixed_from_video_id when video_url is absent', () => {
|
||||
const data = {
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
remixed_from_video_id:
|
||||
'https://platform-outputs.agnes-ai.space/videos/agnes-video-v2.0/2026/06/15/video_7237611b.mp4',
|
||||
video_id: 'video_7237611b',
|
||||
};
|
||||
assert.equal(
|
||||
pickProxyVideoUrl(data),
|
||||
'https://platform-outputs.agnes-ai.space/videos/agnes-video-v2.0/2026/06/15/video_7237611b.mp4'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
applyDeepSeekChatOptions,
|
||||
isDeepSeekOfficialConfig,
|
||||
} = require('../src/services/deepseekConfig');
|
||||
|
||||
function baseBody(model) {
|
||||
return {
|
||||
model,
|
||||
messages: [{ role: 'user', content: 'Hello' }],
|
||||
temperature: 0.7,
|
||||
max_tokens: 5,
|
||||
};
|
||||
}
|
||||
|
||||
test('detects official DeepSeek configs by provider or base URL', () => {
|
||||
assert.equal(isDeepSeekOfficialConfig({ provider: 'deepseek' }), true);
|
||||
assert.equal(isDeepSeekOfficialConfig({ base_url: 'https://api.deepseek.com' }), true);
|
||||
assert.equal(isDeepSeekOfficialConfig({ provider: 'xy', base_url: 'https://api.302.ai/v1' }), false);
|
||||
});
|
||||
|
||||
test('maps deprecated deepseek-chat to deepseek-v4-flash non-thinking mode', () => {
|
||||
const body = applyDeepSeekChatOptions(
|
||||
{ provider: 'deepseek', base_url: 'https://api.deepseek.com' },
|
||||
baseBody('deepseek-chat')
|
||||
);
|
||||
|
||||
assert.equal(body.model, 'deepseek-v4-flash');
|
||||
assert.deepEqual(body.thinking, { type: 'disabled' });
|
||||
assert.equal(body.reasoning_effort, undefined);
|
||||
assert.equal(body.temperature, 0.7);
|
||||
});
|
||||
|
||||
test('maps deprecated deepseek-reasoner to deepseek-v4-flash thinking mode', () => {
|
||||
const body = applyDeepSeekChatOptions(
|
||||
{ provider: 'deepseek', base_url: 'https://api.deepseek.com' },
|
||||
baseBody('deepseek-reasoner')
|
||||
);
|
||||
|
||||
assert.equal(body.model, 'deepseek-v4-flash');
|
||||
assert.deepEqual(body.thinking, { type: 'enabled' });
|
||||
assert.equal(body.temperature, undefined);
|
||||
});
|
||||
|
||||
test('applies explicit DeepSeek thinking settings for V4 models', () => {
|
||||
const body = applyDeepSeekChatOptions(
|
||||
{
|
||||
provider: 'deepseek',
|
||||
base_url: 'https://api.deepseek.com',
|
||||
settings: JSON.stringify({
|
||||
deepseek_thinking: 'enabled',
|
||||
deepseek_reasoning_effort: 'max',
|
||||
}),
|
||||
},
|
||||
baseBody('deepseek-v4-pro')
|
||||
);
|
||||
|
||||
assert.equal(body.model, 'deepseek-v4-pro');
|
||||
assert.deepEqual(body.thinking, { type: 'enabled' });
|
||||
assert.equal(body.reasoning_effort, 'max');
|
||||
assert.equal(body.temperature, undefined);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const Database = require('better-sqlite3');
|
||||
const imageClient = require('../src/services/imageClient');
|
||||
|
||||
function makeDb() {
|
||||
const db = new Database(':memory:');
|
||||
db.exec(`CREATE TABLE image_proxy_cache (
|
||||
cache_key TEXT PRIMARY KEY,
|
||||
proxy_url TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
)`);
|
||||
return db;
|
||||
}
|
||||
|
||||
describe('image_proxy_cache', () => {
|
||||
it('getProxyCache returns null when entry expired by expire_hours', () => {
|
||||
const db = makeDb();
|
||||
const old = new Date(Date.now() - 25 * 3600 * 1000).toISOString();
|
||||
db.prepare(
|
||||
'INSERT INTO image_proxy_cache (cache_key, proxy_url, created_at) VALUES (?, ?, ?)'
|
||||
).run('scenes/test.jpg', 'https://example.com/a.jpg', old);
|
||||
|
||||
assert.equal(imageClient.getProxyCache(db, 'scenes/test.jpg'), null);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) AS c FROM image_proxy_cache').get().c, 0);
|
||||
});
|
||||
|
||||
it('getProxyCache returns url when entry still fresh', () => {
|
||||
const db = makeDb();
|
||||
imageClient.setProxyCache(db, 'scenes/fresh.jpg', 'https://example.com/fresh.jpg');
|
||||
assert.equal(imageClient.getProxyCache(db, 'scenes/fresh.jpg'), 'https://example.com/fresh.jpg');
|
||||
});
|
||||
|
||||
it('deleteProxyCache removes row', () => {
|
||||
const db = makeDb();
|
||||
imageClient.setProxyCache(db, 'k1', 'https://example.com/x.jpg');
|
||||
imageClient.deleteProxyCache(db, 'k1');
|
||||
assert.equal(imageClient.getProxyCache(db, 'k1'), null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
hubBusinessErrorMessage,
|
||||
normalizeMaterialHubToken,
|
||||
tokenFingerprint,
|
||||
unwrapMaterialHubAssetView,
|
||||
} = require('../src/services/jimengMaterialHubService');
|
||||
|
||||
describe('jimengMaterialHub response parsing', () => {
|
||||
it('hubBusinessErrorMessage detects model_ark 200+error body', () => {
|
||||
const msg = hubBusinessErrorMessage({
|
||||
error: '[Failed to download media from the provided URL.]',
|
||||
});
|
||||
assert.match(msg, /download media/i);
|
||||
});
|
||||
|
||||
it('unwrapMaterialHubAssetView parses flat AssetView', () => {
|
||||
const asset = unwrapMaterialHubAssetView({
|
||||
id: 'asset-20260602203139-2vr49',
|
||||
asset_url: 'asset://asset-20260602203139-2vr49',
|
||||
status: 'processing',
|
||||
});
|
||||
assert.equal(asset.id, 'asset-20260602203139-2vr49');
|
||||
assert.equal(asset.status, 'processing');
|
||||
});
|
||||
|
||||
it('unwrapMaterialHubAssetView parses data wrapper', () => {
|
||||
const asset = unwrapMaterialHubAssetView({
|
||||
data: { asset_id: 'AST-1', status: 'active', asset_url: 'asset://x' },
|
||||
});
|
||||
assert.equal(asset.id, 'AST-1');
|
||||
});
|
||||
|
||||
it('unwrapMaterialHubAssetView returns null when only error field', () => {
|
||||
assert.equal(unwrapMaterialHubAssetView({ error: 'failed' }), null);
|
||||
});
|
||||
|
||||
it('normalizeMaterialHubToken strips Bearer and zero-width chars', () => {
|
||||
const t = normalizeMaterialHubToken('Bearer sk-test\u200bkey\u200b');
|
||||
assert.equal(t, 'sk-testkey');
|
||||
});
|
||||
|
||||
it('tokenFingerprint shows head and tail only', () => {
|
||||
assert.equal(tokenFingerprint('sk-abcdefghijklmnop'), 'sk-abcd…mnop');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
const characterLibraryService = require('../src/services/characterLibraryService');
|
||||
const sceneLibraryService = require('../src/services/sceneLibraryService');
|
||||
const propLibraryService = require('../src/services/propLibraryService');
|
||||
|
||||
const log = {
|
||||
info() {},
|
||||
warn() {},
|
||||
error() {},
|
||||
};
|
||||
|
||||
function createDb() {
|
||||
const db = new Database(':memory:');
|
||||
db.exec(`
|
||||
CREATE TABLE dramas (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE characters (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
drama_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
description TEXT,
|
||||
appearance TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE scenes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
drama_id INTEGER NOT NULL,
|
||||
location TEXT,
|
||||
time TEXT,
|
||||
prompt TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE props (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
drama_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
type TEXT,
|
||||
description TEXT,
|
||||
prompt TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE character_libraries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
drama_id INTEGER,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
category TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
description TEXT,
|
||||
tags TEXT,
|
||||
source_type TEXT,
|
||||
source_id TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE scene_libraries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
drama_id INTEGER,
|
||||
location TEXT NOT NULL DEFAULT '',
|
||||
time TEXT,
|
||||
prompt TEXT,
|
||||
description TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
category TEXT,
|
||||
tags TEXT,
|
||||
source_type TEXT,
|
||||
source_id TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE prop_libraries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
drama_id INTEGER,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
description TEXT,
|
||||
prompt TEXT,
|
||||
image_url TEXT,
|
||||
local_path TEXT,
|
||||
category TEXT,
|
||||
tags TEXT,
|
||||
source_type TEXT,
|
||||
source_id TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
);
|
||||
`);
|
||||
db.prepare('INSERT INTO dramas (id, title) VALUES (1, ?)').run('Test drama');
|
||||
return db;
|
||||
}
|
||||
|
||||
function countRows(db, table, where) {
|
||||
return db.prepare(`SELECT COUNT(*) AS count FROM ${table} WHERE ${where}`).get().count;
|
||||
}
|
||||
|
||||
test('adding the same character to drama and material libraries is idempotent', () => {
|
||||
const db = createDb();
|
||||
db.prepare(
|
||||
'INSERT INTO characters (id, drama_id, name, description, image_url, local_path) VALUES (1, 1, ?, ?, ?, ?)'
|
||||
).run('Hero', 'original', '/static/projects/hero.png', 'projects/hero.png');
|
||||
|
||||
const firstDrama = characterLibraryService.addCharacterToLibrary(db, log, 1);
|
||||
db.prepare('UPDATE characters SET name = ?, description = ? WHERE id = 1').run('Hero updated', 'updated');
|
||||
const secondDrama = characterLibraryService.addCharacterToLibrary(db, log, 1);
|
||||
const firstMaterial = characterLibraryService.addCharacterToMaterialLibrary(db, log, 1);
|
||||
const secondMaterial = characterLibraryService.addCharacterToMaterialLibrary(db, log, 1);
|
||||
|
||||
assert.equal(firstDrama.item.id, secondDrama.item.id);
|
||||
assert.equal(firstMaterial.item.id, secondMaterial.item.id);
|
||||
assert.equal(countRows(db, 'character_libraries', 'drama_id = 1 AND deleted_at IS NULL'), 1);
|
||||
assert.equal(countRows(db, 'character_libraries', 'drama_id IS NULL AND deleted_at IS NULL'), 1);
|
||||
assert.equal(secondDrama.item.name, 'Hero updated');
|
||||
assert.equal(
|
||||
db.prepare('SELECT source_id FROM character_libraries WHERE id = ?').get(secondDrama.item.id).source_id,
|
||||
'1'
|
||||
);
|
||||
});
|
||||
|
||||
test('adding the same scene to drama and material libraries is idempotent', () => {
|
||||
const db = createDb();
|
||||
db.prepare(
|
||||
'INSERT INTO scenes (id, drama_id, location, time, prompt, image_url, local_path) VALUES (1, 1, ?, ?, ?, ?, ?)'
|
||||
).run('Village', 'day', 'quiet street', '/static/projects/village.png', 'projects/village.png');
|
||||
|
||||
const firstDrama = sceneLibraryService.addSceneToLibrary(db, log, 1);
|
||||
db.prepare('UPDATE scenes SET location = ?, prompt = ? WHERE id = 1').run('Village updated', 'busy street');
|
||||
const secondDrama = sceneLibraryService.addSceneToLibrary(db, log, 1);
|
||||
const firstMaterial = sceneLibraryService.addSceneToMaterialLibrary(db, log, 1);
|
||||
const secondMaterial = sceneLibraryService.addSceneToMaterialLibrary(db, log, 1);
|
||||
|
||||
assert.equal(firstDrama.item.id, secondDrama.item.id);
|
||||
assert.equal(firstMaterial.item.id, secondMaterial.item.id);
|
||||
assert.equal(countRows(db, 'scene_libraries', 'drama_id = 1 AND deleted_at IS NULL'), 1);
|
||||
assert.equal(countRows(db, 'scene_libraries', 'drama_id IS NULL AND deleted_at IS NULL'), 1);
|
||||
assert.equal(secondDrama.item.location, 'Village updated');
|
||||
assert.equal(
|
||||
db.prepare('SELECT source_id FROM scene_libraries WHERE id = ?').get(secondDrama.item.id).source_id,
|
||||
'1'
|
||||
);
|
||||
});
|
||||
|
||||
test('adding the same prop to drama and material libraries is idempotent', () => {
|
||||
const db = createDb();
|
||||
db.prepare(
|
||||
'INSERT INTO props (id, drama_id, name, description, prompt, image_url, local_path) VALUES (1, 1, ?, ?, ?, ?, ?)'
|
||||
).run('Sword', 'old blade', 'silver sword', '/static/projects/sword.png', 'projects/sword.png');
|
||||
|
||||
const firstDrama = propLibraryService.addPropToLibrary(db, log, 1);
|
||||
db.prepare('UPDATE props SET name = ?, description = ? WHERE id = 1').run('Sword updated', 'polished blade');
|
||||
const secondDrama = propLibraryService.addPropToLibrary(db, log, 1);
|
||||
const firstMaterial = propLibraryService.addPropToMaterialLibrary(db, log, 1);
|
||||
const secondMaterial = propLibraryService.addPropToMaterialLibrary(db, log, 1);
|
||||
|
||||
assert.equal(firstDrama.item.id, secondDrama.item.id);
|
||||
assert.equal(firstMaterial.item.id, secondMaterial.item.id);
|
||||
assert.equal(countRows(db, 'prop_libraries', 'drama_id = 1 AND deleted_at IS NULL'), 1);
|
||||
assert.equal(countRows(db, 'prop_libraries', 'drama_id IS NULL AND deleted_at IS NULL'), 1);
|
||||
assert.equal(secondDrama.item.name, 'Sword updated');
|
||||
assert.equal(
|
||||
db.prepare('SELECT source_id FROM prop_libraries WHERE id = ?').get(secondDrama.item.id).source_id,
|
||||
'1'
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user