Files
open-design/apps/daemon/tests/media-tasks-routes.test.ts
b578b93f3f Bug FIx: Media generation task state is volatile and lost on daemon restart #648 (#884)
* feat: implement media tasks persistence

* fix(daemon): satisfy exactOptionalPropertyTypes in media-tasks-routes test

`process.env.OD_DATA_DIR` is `string | undefined`, but `openDatabase`'s
options accept `{ dataDir?: string }`. Under the daemon tsconfig's
exactOptionalPropertyTypes the two are not assignable. Spread the key
in only when defined.

* fix(daemon): restore mcp-config / mcp-oauth / mcp-tokens imports in server.ts

The earlier 'Merge branch main into main' resolved the import-block
conflict by keeping only the new media-tasks import and dropping the
three pre-existing import blocks. server.ts still uses 13+ symbols
from those modules (PendingAuthCache, MCP_TEMPLATES, beginAuth,
readMcpConfig, getToken, etc.), so the daemon crashed at startup with
'ReferenceError: PendingAuthCache is not defined' the moment Playwright
booted it. Restore the three import blocks verbatim from main.

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-05-09 00:00:18 +08:00

69 lines
2.1 KiB
TypeScript

import type http from 'node:http';
import { afterEach, describe, expect, it } from 'vitest';
import { randomUUID } from 'node:crypto';
import { closeDatabase, insertProject, openDatabase } from '../src/db.js';
import { insertMediaTask } from '../src/media-tasks.js';
import { startServer } from '../src/server.js';
describe('media task route recovery', () => {
let server: http.Server | null = null;
afterEach(async () => {
if (server) {
await new Promise<void>((resolve) => server?.close(() => resolve()));
server = null;
}
closeDatabase();
});
it('recovers a pre-restart running task so wait returns interrupted instead of 404', async () => {
const dataDir = process.env.OD_DATA_DIR;
const db = openDatabase(process.cwd(), dataDir === undefined ? {} : { dataDir });
const projectId = `project_${randomUUID()}`;
const taskId = `task_${randomUUID()}`;
const now = Date.now() - 5_000;
insertProject(db, {
id: projectId,
name: 'Recovered media project',
createdAt: now,
updatedAt: now,
});
insertMediaTask(db, {
id: taskId,
projectId,
status: 'running',
surface: 'video',
model: 'seedance-2',
progress: ['provider task accepted'],
startedAt: now,
updatedAt: now,
});
const started = await startServer({ port: 0, returnServer: true }) as {
url: string;
server: http.Server;
};
server = started.server;
const response = await fetch(`${started.url}/api/media/tasks/${encodeURIComponent(taskId)}/wait`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ since: 0, timeoutMs: 0 }),
});
const body = await response.json() as {
status?: string;
progress?: string[];
error?: { code?: string; message?: string };
};
expect(response.status).toBe(200);
expect(body.status).toBe('interrupted');
expect(body.progress).toEqual(['provider task accepted']);
expect(body.error).toMatchObject({
code: 'DAEMON_RESTART',
message: 'media task interrupted by daemon restart',
});
});
});