205 lines
6.3 KiB
TypeScript
205 lines
6.3 KiB
TypeScript
import { execFileSync } from 'node:child_process';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import type { AuthUser } from './auth.js';
|
|
import { finalizeShow, getCompletionSummary } from './completion.js';
|
|
import { closeDatabase, getDatabase } from './database.js';
|
|
import {
|
|
addPieceUsage,
|
|
addShowSection,
|
|
createShow,
|
|
duplicateCompletedShow,
|
|
getPieceUsageContexts,
|
|
getShow
|
|
} from './shows.js';
|
|
|
|
let directory: string;
|
|
const actor: AuthUser = {
|
|
id: 'completion-actor',
|
|
email: 'completion@example.test',
|
|
displayName: 'Abschluss Test',
|
|
role: 'barkeeper',
|
|
lockedAtUtc: null
|
|
};
|
|
|
|
beforeEach(() => {
|
|
directory = mkdtempSync(path.join(tmpdir(), 'sendewerk-completion-test-'));
|
|
process.env.SENDEWERK_DB_PATH = path.join(directory, 'test.sqlite');
|
|
process.env.SENDEWERK_UPLOAD_DIR = path.join(directory, 'uploads');
|
|
execFileSync(process.execPath, ['scripts/migrate.mjs'], { cwd: process.cwd(), env: process.env });
|
|
execFileSync(process.execPath, ['scripts/seed.mjs'], { cwd: process.cwd(), env: process.env });
|
|
getDatabase()
|
|
.prepare(
|
|
`INSERT INTO users (id, email, email_normalized, display_name, password_hash, role,
|
|
password_changed_at_utc, created_at_utc, updated_at_utc)
|
|
VALUES (?, ?, ?, ?, 'unused', 'barkeeper', ?, ?, ?)`
|
|
)
|
|
.run(
|
|
actor.id,
|
|
actor.email,
|
|
actor.email,
|
|
actor.displayName,
|
|
...Array(3).fill(new Date().toISOString())
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
closeDatabase();
|
|
delete process.env.SENDEWERK_DB_PATH;
|
|
delete process.env.SENDEWERK_UPLOAD_DIR;
|
|
rmSync(directory, { recursive: true, force: true });
|
|
});
|
|
|
|
function endedShow() {
|
|
const created = createShow({ title: 'Finale Sendung', startsAtUtc: null, templateId: null });
|
|
const section = addShowSection(created.id, created.revision, {
|
|
name: 'Gesendeter Block',
|
|
categoryId: 'cat-jc',
|
|
explicitDurationSeconds: null
|
|
});
|
|
const done = addPieceUsage(created.id, section.revision, {
|
|
pieceId: 'piece-console',
|
|
sectionId: section.id
|
|
});
|
|
const skipped = addPieceUsage(created.id, done.revision, {
|
|
pieceId: 'piece-console',
|
|
sectionId: section.id
|
|
});
|
|
const database = getDatabase();
|
|
database.prepare("UPDATE piece_usages SET status = 'done' WHERE id = ?").run(done.id);
|
|
database.prepare("UPDATE piece_usages SET status = 'skipped' WHERE id = ?").run(skipped.id);
|
|
database
|
|
.prepare(
|
|
`INSERT INTO show_live_states
|
|
(show_id, revision, started_at_utc, ended_at_utc, presentation_revision, updated_at_utc)
|
|
VALUES (?, 0, ?, ?, 0, ?)`
|
|
)
|
|
.run(
|
|
created.id,
|
|
'2026-10-25T18:00:00.000Z',
|
|
'2026-10-25T19:00:00.000Z',
|
|
'2026-10-25T19:00:00.000Z'
|
|
);
|
|
return { showId: created.id, doneUsageId: done.id, revision: skipped.revision };
|
|
}
|
|
|
|
describe('show completion', () => {
|
|
it('finalizes once, records only sent usages and archives only the explicit selection', () => {
|
|
const prepared = endedShow();
|
|
expect(getCompletionSummary(prepared.showId)).toMatchObject({
|
|
doneCount: 1,
|
|
skippedCount: 1,
|
|
candidates: [{ pieceId: 'piece-console', usageCount: 1 }]
|
|
});
|
|
const first = finalizeShow({
|
|
showId: prepared.showId,
|
|
commandId: 'complete-once',
|
|
expectedShowRevision: prepared.revision,
|
|
expectedLiveRevision: 0,
|
|
archivePieceIds: ['piece-console'],
|
|
actor,
|
|
now: new Date('2026-10-25T19:05:00.000Z')
|
|
});
|
|
expect(first.duplicate).toBe(false);
|
|
expect(
|
|
finalizeShow({
|
|
showId: prepared.showId,
|
|
commandId: 'complete-once',
|
|
expectedShowRevision: prepared.revision,
|
|
expectedLiveRevision: 0,
|
|
archivePieceIds: ['piece-console'],
|
|
actor
|
|
}).duplicate
|
|
).toBe(true);
|
|
expect(() =>
|
|
finalizeShow({
|
|
showId: prepared.showId,
|
|
commandId: 'different-command',
|
|
expectedShowRevision: prepared.revision,
|
|
expectedLiveRevision: 0,
|
|
archivePieceIds: [],
|
|
actor
|
|
})
|
|
).toThrow('SHOW_COMPLETED');
|
|
const database = getDatabase();
|
|
expect(
|
|
(
|
|
database
|
|
.prepare('SELECT count(*) AS count FROM piece_usage_history WHERE show_id = ?')
|
|
.get(prepared.showId) as { count: number }
|
|
).count
|
|
).toBe(1);
|
|
expect(database.prepare('SELECT status FROM pieces WHERE id = ?').get('piece-console')).toEqual(
|
|
{ status: 'archived' }
|
|
);
|
|
expect(getPieceUsageContexts(['piece-console'])['piece-console'].sentHistory).toHaveLength(1);
|
|
});
|
|
|
|
it('keeps historical snapshots stable and duplicates them into an editable clean run', () => {
|
|
const prepared = endedShow();
|
|
finalizeShow({
|
|
showId: prepared.showId,
|
|
commandId: 'freeze',
|
|
expectedShowRevision: prepared.revision,
|
|
expectedLiveRevision: 0,
|
|
archivePieceIds: [],
|
|
actor
|
|
});
|
|
const historical = getShow(prepared.showId)!;
|
|
getDatabase()
|
|
.prepare("UPDATE section_categories SET name = 'Später geändert' WHERE id = 'cat-jc'")
|
|
.run();
|
|
getDatabase()
|
|
.prepare("UPDATE pieces SET title = 'Später geändert' WHERE id = 'piece-console'")
|
|
.run();
|
|
const unchanged = getShow(prepared.showId)!;
|
|
expect(unchanged.sections[0].category).toBe(historical.sections[0].category);
|
|
expect(unchanged.sections[0].usages[0].title).toBe(historical.sections[0].usages[0].title);
|
|
|
|
const copy = duplicateCompletedShow(prepared.showId, 'copy-once');
|
|
expect(duplicateCompletedShow(prepared.showId, 'copy-once').id).toBe(copy.id);
|
|
expect(getShow(copy.id)).toMatchObject({
|
|
status: 'draft',
|
|
startsAtUtc: null,
|
|
sections: [
|
|
{
|
|
category: historical.sections[0].category,
|
|
usages: [{ status: 'open' }, { status: 'open' }]
|
|
}
|
|
]
|
|
});
|
|
expect(
|
|
getDatabase().prepare('SELECT 1 FROM show_finalizations WHERE show_id = ?').get(copy.id)
|
|
).toBeUndefined();
|
|
getDatabase()
|
|
.prepare(
|
|
`UPDATE piece_usages SET status = 'done'
|
|
WHERE show_section_id IN (SELECT id FROM show_sections WHERE show_id = ?)`
|
|
)
|
|
.run(copy.id);
|
|
getDatabase()
|
|
.prepare(
|
|
`INSERT INTO show_live_states
|
|
(show_id, revision, started_at_utc, ended_at_utc, presentation_revision, updated_at_utc)
|
|
VALUES (?, 0, ?, ?, 0, ?)`
|
|
)
|
|
.run(
|
|
copy.id,
|
|
'2026-10-26T18:00:00.000Z',
|
|
'2026-10-26T19:00:00.000Z',
|
|
'2026-10-26T19:00:00.000Z'
|
|
);
|
|
finalizeShow({
|
|
showId: copy.id,
|
|
commandId: 'finalize-copy',
|
|
expectedShowRevision: 1,
|
|
expectedLiveRevision: 0,
|
|
archivePieceIds: [],
|
|
actor
|
|
});
|
|
expect(getShow(copy.id)?.sections[0].category).toBe(historical.sections[0].category);
|
|
});
|
|
});
|