Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions server/services/activeProcessing.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getCudaCapability, getCudaUtilization } from '../lib/cudaCapability.js';
import { listJobs, getRunningJob } from './mediaJobQueue/index.js';
import { sanitizeJob } from './mediaJobQueue/sanitizeJob.js';
import { listModels } from './imageTo3d/models.js';
import { listGeneratingModelSummaries } from './imageTo3d/models.js';
import { getLoadedModels } from './ollamaManager.js';
import * as cos from './cos.js';

Expand All @@ -11,7 +11,7 @@ export async function getActiveProcessing() {
const [capability, jobs, models, loadedModels, taskData, agents] = await Promise.all([
getCudaCapability(),
Promise.resolve(listJobs()).then((items) => items.filter((job) => LIVE_STATUSES.has(job.status))),
listModels().catch(() => []),
listGeneratingModelSummaries().catch(() => []),
getLoadedModels().catch(() => []),
cos.getAllTasks().catch(() => ({ user: {}, cos: {} })),
// `null` = the read FAILED, distinct from `[]` = read fine, no agents. The
Expand Down Expand Up @@ -51,7 +51,7 @@ export async function getActiveProcessing() {
},
jobs: jobs.map(sanitizeJob),
extras: {
imageTo3d: models.filter((model) => model.status === 'generating').map((model) => ({ id: model.id, name: model.name || model.id })),
imageTo3d: models.map((model) => ({ id: model.id, name: model.name || model.id })),
ollama: loadedModels,
},
agents: {
Expand Down
9 changes: 6 additions & 3 deletions server/services/activeProcessing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const deps = vi.hoisted(() => ({
vi.mock('../lib/cudaCapability.js', () => ({ getCudaCapability: deps.capability, getCudaUtilization: deps.utilization }));
vi.mock('./mediaJobQueue/index.js', () => ({ listJobs: deps.jobs, getRunningJob: deps.running }));
vi.mock('./mediaJobQueue/sanitizeJob.js', () => ({ sanitizeJob: (job) => ({ id: job.id, kind: job.kind, status: job.status, params: { musicStudio: job.params.musicStudio } }) }));
vi.mock('./imageTo3d/models.js', () => ({ listModels: deps.models }));
vi.mock('./imageTo3d/models.js', () => ({ listGeneratingModelSummaries: deps.models }));
vi.mock('./ollamaManager.js', () => ({ getLoadedModels: deps.loaded }));
vi.mock('./cos.js', () => ({ getAllTasks: deps.tasks, getStatus: deps.status, getAgents: deps.agents }));

Expand All @@ -22,7 +22,7 @@ describe('active processing snapshot', () => {
deps.utilization.mockResolvedValue({ status: 'available', gpus: [{ name: 'Example GPU', utilizationPercent: 44, memoryUsedMib: 1000, memoryTotalMib: 24000 }] });
deps.jobs.mockReturnValue([{ id: 'audio-1', kind: 'audio', status: 'running', params: { prompt: 'fake', musicStudio: { trackId: 'track-1' }, secretPath: '/private' } }]);
deps.running.mockReturnValue({ kind: 'audio' });
deps.models.mockResolvedValue([{ id: 'mesh-1', name: 'Fake mesh', status: 'generating' }]);
deps.models.mockResolvedValue([{ id: 'mesh-1', name: 'Fake mesh' }, { id: 'mesh-2', name: '' }]);
deps.loaded.mockResolvedValue([{ id: 'model-1', name: 'Fake model' }]);
deps.tasks.mockResolvedValue({ user: { tasks: [{ id: 'task-1', status: 'pending' }] }, cos: { tasks: [{ id: 'cos-task-1', status: 'completed' }] } });
deps.status.mockResolvedValue({ activeAgents: 99 });
Expand All @@ -34,7 +34,7 @@ describe('active processing snapshot', () => {
expect(snapshot.jobs).toHaveLength(1);
expect(snapshot.gpu).toMatchObject({ status: 'available', laneBusy: true, laneKind: 'audio' });
expect(snapshot.gpu.gpus[0]).toMatchObject({ utilizationPercent: 44, memoryUsedMib: 1000 });
expect(snapshot.extras.imageTo3d).toEqual([{ id: 'mesh-1', name: 'Fake mesh' }]);
expect(snapshot.extras.imageTo3d).toEqual([{ id: 'mesh-1', name: 'Fake mesh' }, { id: 'mesh-2', name: 'mesh-2' }]);
expect(snapshot.extras.ollama).toEqual([{ id: 'model-1', name: 'Fake model' }]);
expect(snapshot.agents).toEqual({ active: 2, queued: 1 });
expect(deps.status).not.toHaveBeenCalled();
Expand All @@ -51,6 +51,9 @@ describe('active processing snapshot', () => {
deps.agents.mockResolvedValue([]);
const snapshot = await getActiveProcessing();
expect(snapshot.gpu).toMatchObject({ status: 'absent', laneBusy: false, laneKind: null, gpus: [] });
expect(snapshot.extras.imageTo3d).toEqual([]);
deps.models.mockRejectedValueOnce(new Error('store unavailable'));
expect((await getActiveProcessing()).extras.imageTo3d).toEqual([]);
expect(deps.utilization).not.toHaveBeenCalled();
});
});
Expand Down
7 changes: 7 additions & 0 deletions server/services/imageTo3d/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ export async function listModels({ includeDeleted = false } = {}) {
return result.rows.map(rowToModel);
}

export async function listGeneratingModelSummaries() {
const result = await query(
"SELECT id, name FROM image_to_3d_models WHERE deleted = FALSE AND status = 'generating' ORDER BY updated_at DESC",
);
return result.rows;
}

export async function getModel(id, { includeDeleted = false } = {}) {
const result = await query('SELECT data FROM image_to_3d_models WHERE id = $1', [id]);
const model = rowToModel(result.rows[0]);
Expand Down
62 changes: 62 additions & 0 deletions server/services/imageTo3d/db.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import * as connection from '../../lib/db.js';
import { requireDbOrSkip } from '../../lib/dbTestGate.js';
import { listGeneratingModelSummaries, listModels } from './db.js';

const health = await connection.checkHealth();
const runDb = requireDbOrSkip('image-to-3D summaries', health.connected, health.error);

describe.skipIf(!runDb)('image-to-3D activity projection', () => {
beforeAll(async () => {
await connection.ensureSchema();
await connection.query('DELETE FROM image_to_3d_models');
});

afterAll(async () => {
vi.restoreAllMocks();
await connection.query('DELETE FROM image_to_3d_models');
await connection.close();
});

it('returns only live generating id/name rows in update order without hydrating gallery records', async () => {
expect(await listGeneratingModelSummaries()).toEqual([]);

const completed = Array.from({ length: 1000 }, (_, i) => ({
id: `completed-${i}`, name: 'Example completed mesh', status: 'completed',
updatedAt: '2026-01-01T00:00:00Z', deleted: false,
runs: [{ output: 'Example generation history' }],
}));
const others = [
{ id: 'draft', status: 'draft' },
{ id: 'failed', status: 'failed' },
{ id: 'deleted', status: 'generating', deleted: true },
{ id: 'older', status: 'generating', updatedAt: '2026-01-02T00:00:00Z', name: '' },
{ id: 'newer', status: 'generating', updatedAt: '2026-01-03T00:00:00Z' },
].map((record) => ({
name: 'Example mesh', updatedAt: '2026-01-01T00:00:00Z', deleted: false, ...record,
}));
await connection.query(
`INSERT INTO image_to_3d_models (id, name, status, deleted, updated_at, data)
SELECT value->>'id', value->>'name', value->>'status',
(value->>'deleted')::boolean, (value->>'updatedAt')::timestamptz, value
FROM jsonb_array_elements($1::jsonb)`,
[JSON.stringify([...completed, ...others])],
);

const querySpy = vi.spyOn(connection, 'query');
expect(await listGeneratingModelSummaries()).toEqual([
{ id: 'newer', name: 'Example mesh' },
{ id: 'older', name: '' },
]);
// A row-shape assertion alone would allow SELECT * followed by JS projection.
expect(querySpy).toHaveBeenCalledExactlyOnceWith(
"SELECT id, name FROM image_to_3d_models WHERE deleted = FALSE AND status = 'generating' ORDER BY updated_at DESC",
);
querySpy.mockRestore();

const gallery = await listModels();
expect(gallery).toHaveLength(1004);
expect(gallery.find((model) => model.id === 'completed-0')).toEqual(completed[0]);
expect(await listModels({ includeDeleted: true })).toHaveLength(1005);
});
});
1 change: 1 addition & 0 deletions server/services/imageTo3d/models.genericDispatch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ vi.mock('../../lib/heavyJobClaim.js', () => ({

vi.mock('./db.js', () => ({
listModels: vi.fn(),
listGeneratingModelSummaries: vi.fn(),
getModel: vi.fn(),
createModel: vi.fn(),
mutateModel: vi.fn(),
Expand Down
1 change: 1 addition & 0 deletions server/services/imageTo3d/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ async function executeRender({ id, operationId, adapter, sourcePath, caps, optio
}

export const listModels = store.listModels;
export const listGeneratingModelSummaries = store.listGeneratingModelSummaries;
export const getModel = store.getModel;

/**
Expand Down
1 change: 1 addition & 0 deletions server/services/imageTo3d/models.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ vi.mock('../localMemory.js', async (importOriginal) => ({

vi.mock('./db.js', () => ({
listModels: vi.fn(),
listGeneratingModelSummaries: vi.fn(),
getModel: vi.fn(),
createModel: vi.fn(),
mutateModel: vi.fn(),
Expand Down