Seed z repozitare, zatezove testy, worker bez spanku, dialogy bez rozmazani
Nastaveni prezije nasazeni: seed/records.json se pri prazdnem ulozisti nacte misto ukazkovych dat (zive uloziste se nikdy neprepisuje). Soubor nese soucasny stav produkce (firmy s provozovatelem, role, typ ticketu, akce, widgety, skupiny, rozlozeni). Novy GET /api/admin/export a skript npm run seed:export pro dalsi exporty, Dockerfile slozku kopiruje. Vykonnostni testy (npm run test:perf) nad 200 firmami a 10 000 tickety a zatezovy skript (npm run load) proti bezici instanci vcetne davky udalosti na webhook. Mereni odhalilo strop workeru: po obsazeni vsech mist spal sekundu, takze fronta odbavila nejvys 4 behy za sekundu. Ted ceka na prvni dokonceny beh: 500 udalosti za 1,3 s (395 behu/s). Strop posluchacu streamu zvednut na 2 000. Dialogy: prekryv modalu a menu v portalu bez backdrop-blur, tecka Zive pulzuje jen pri navazovani spojeni - rozmazani cele obrazovky pod trvalou animaci sekalo video vedle portalu. Bublina udalosti drzi 0,5 s. Dokumentace 14, 19, 20, 22, 04, 01, 03, 15 a 99 aktualizovana. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c25e826766
commit
ea9387bea9
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Nastaveni z repozitare (src/data/store/seedFile.ts).
|
||||
*
|
||||
* Bezi v rezimu souboru nad docasnou slozkou, aby slo pred startem podstrcit
|
||||
* neprazdne uloziste (snapshot `ticketType.json`) a vedle nej seed soubor.
|
||||
* Pak se spusti stejny bootstrap jako v `src/index.ts` a overi se, co ktere
|
||||
* uloziste dostalo. `DATA_DIR` a `SEED_FILE` se ctou pri importu konfigurace,
|
||||
* proto dynamicke importy.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'csbot-seed-'));
|
||||
const dataDir = path.join(dir, 'data');
|
||||
const seedPath = path.join(dir, 'records.json');
|
||||
process.env.DATA_DIR = dataDir;
|
||||
process.env.SEED_FILE = seedPath;
|
||||
|
||||
type Tenants = typeof import('../../src/data/tenants.js');
|
||||
type TicketTypes = typeof import('../../src/data/ticketTypes.js');
|
||||
type Widgets = typeof import('../../src/data/customWidgets.js');
|
||||
type Stores = typeof import('../../src/data/store/index.js');
|
||||
type SeedFile = typeof import('../../src/data/store/seedFile.js');
|
||||
|
||||
let tenants: Tenants;
|
||||
let ticketTypes: TicketTypes;
|
||||
let widgets: Widgets;
|
||||
let stores: Stores;
|
||||
let seedFile: SeedFile;
|
||||
/** Co start zalogoval pres `console.warn`. */
|
||||
let warnings: string[] = [];
|
||||
|
||||
const now = '2026-09-09T08:00:00.000Z';
|
||||
|
||||
function snapshot(kind: string, items: unknown[]): void {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dataDir, `${kind}.json`),
|
||||
JSON.stringify({ version: 1, savedAt: now, items }, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
|
||||
function ticketType(id: string, key: string) {
|
||||
return {
|
||||
id,
|
||||
tenantId: 'tnt_seed',
|
||||
key,
|
||||
name: key,
|
||||
icon: 'Tag',
|
||||
statuses: [],
|
||||
fields: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
// Typy ticketu uz v ulozisti jsou: soubor je nesmi prepsat.
|
||||
snapshot('ticketType', [ticketType('tt_existing', 'existing')]);
|
||||
|
||||
fs.writeFileSync(
|
||||
seedPath,
|
||||
JSON.stringify({
|
||||
exportedAt: now,
|
||||
kinds: {
|
||||
tenant: [
|
||||
{
|
||||
id: 'tnt_seed',
|
||||
tenantId: null,
|
||||
name: 'Firma ze souboru',
|
||||
note: '',
|
||||
enabled: true,
|
||||
intakeToken: 'seed-intake-token-0123456789',
|
||||
helpdeskProviderId: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
],
|
||||
ticketType: [ticketType('tt_fromfile', 'fromfile')],
|
||||
// Prazdny seznam znamena "zadne", ne "doplnte vychozi z kodu".
|
||||
customWidget: [],
|
||||
// Provozni data se ze souboru nenacitaji.
|
||||
ticket: [{ id: 'TK-1', tenantId: 'tnt_seed', subject: 'x' }],
|
||||
neznamy: [{ id: 'x' }],
|
||||
},
|
||||
}),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const bootstrap = await import('../../src/data/bootstrap.js');
|
||||
tenants = await import('../../src/data/tenants.js');
|
||||
ticketTypes = await import('../../src/data/ticketTypes.js');
|
||||
widgets = await import('../../src/data/customWidgets.js');
|
||||
stores = await import('../../src/data/store/index.js');
|
||||
seedFile = await import('../../src/data/store/seedFile.js');
|
||||
|
||||
const mode = await bootstrap.bootstrapData({ databaseReady: false });
|
||||
// Volani se berou hned po startu: vitest mock mezi testy cisti.
|
||||
warnings = vi.mocked(console.warn).mock.calls.map((call) => String(call[0]));
|
||||
expect(mode).toBe('file');
|
||||
await stores.flushStores();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await stores?.flushStores();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('seed ze souboru pri startu', () => {
|
||||
test('prazdne uloziste dostane zaznamy ze souboru misto seedu z kodu', () => {
|
||||
const ids = tenants.listTenants().map((tenant) => tenant.id);
|
||||
expect(ids).toEqual(['tnt_seed']);
|
||||
expect(tenants.findByIntakeToken('seed-intake-token-0123456789')?.name).toBe(
|
||||
'Firma ze souboru',
|
||||
);
|
||||
});
|
||||
|
||||
test('neprazdne uloziste si necha svoje zaznamy', () => {
|
||||
expect(ticketTypes.findTicketType('tt_existing')).toBeDefined();
|
||||
expect(ticketTypes.findTicketType('tt_fromfile')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('druh v souboru vyhrava, i kdyz je prazdny', async () => {
|
||||
expect(widgets.seedCustomWidgets().length).toBeGreaterThan(0);
|
||||
expect(await widgets.customWidgetStore.listAll()).toEqual([]);
|
||||
});
|
||||
|
||||
test('druh mimo konfiguraci se ignoruje a zaloguje', async () => {
|
||||
expect(warnings.some((line) => line.includes('druh ticket se nenacita'))).toBe(true);
|
||||
expect(warnings.some((line) => line.includes('druh neznamy se nenacita'))).toBe(true);
|
||||
expect(await stores.storeByKind('ticket')?.listAll()).toEqual([]);
|
||||
});
|
||||
|
||||
test('soubor se cte jednou a uloziste dostava kopii', () => {
|
||||
const first = seedFile.seedFromFile<{ id: string; tenantId: null; createdAt: string; updatedAt: string }>(
|
||||
'tenant',
|
||||
)?.();
|
||||
const second = seedFile.seedFromFile<{ id: string; tenantId: null; createdAt: string; updatedAt: string }>(
|
||||
'tenant',
|
||||
)?.();
|
||||
expect(first).toEqual(second);
|
||||
expect(first?.[0]).not.toBe(second?.[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('seedFromFile bez pouzitelneho souboru', () => {
|
||||
const fallback = () => [{ id: 'tnt_code', tenantId: null, createdAt: now, updatedAt: now }];
|
||||
|
||||
test('chybejici soubor je ticho a jede se z kodu', () => {
|
||||
const errors = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
const seed = seedFile.seedFromFile('tenant', fallback, path.join(dir, 'neni.json'));
|
||||
expect(seed?.()).toEqual(fallback());
|
||||
expect(errors).not.toHaveBeenCalled();
|
||||
errors.mockRestore();
|
||||
});
|
||||
|
||||
test('rozbity JSON se zaloguje a jede se z kodu', () => {
|
||||
const errors = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
const file = path.join(dir, 'rozbity.json');
|
||||
fs.writeFileSync(file, '{ "kinds": ', 'utf8');
|
||||
const seed = seedFile.seedFromFile('tenant', fallback, file);
|
||||
expect(seed?.()).toEqual(fallback());
|
||||
expect(errors).toHaveBeenCalledWith(expect.stringContaining('neni platny JSON'), expect.anything());
|
||||
errors.mockRestore();
|
||||
});
|
||||
|
||||
test('zaznam bez id je spatny tvar a jede se z kodu', () => {
|
||||
const errors = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
const file = path.join(dir, 'bez-id.json');
|
||||
fs.writeFileSync(file, JSON.stringify({ kinds: { tenant: [{ name: 'x' }] } }), 'utf8');
|
||||
expect(seedFile.loadSeedFile(file)).toBeNull();
|
||||
expect(seedFile.seedFromFile('tenant', fallback, file)?.()).toEqual(fallback());
|
||||
expect(errors).toHaveBeenCalledWith(expect.stringContaining('kinds.tenant.0.id'));
|
||||
errors.mockRestore();
|
||||
});
|
||||
|
||||
test('bez souboru a bez seedu z kodu neni co nasypat', () => {
|
||||
expect(seedFile.seedFromFile('tenantScript', undefined, path.join(dir, 'neni.json'))).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user