task-d.7.10
This commit is contained in:
44
src/tests/setup/golden-dataset.test.ts
Normal file
44
src/tests/setup/golden-dataset.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { assertFileExists, assertHeadersMatchTemplate, countRows, parseCsv, readJson } from './helpers.ts';
|
||||
|
||||
interface DatasetManifest {
|
||||
files: string[];
|
||||
counts: Record<string, number>;
|
||||
expectedQuotationStatuses: string[];
|
||||
}
|
||||
|
||||
test('golden dataset files exist and match template headers', () => {
|
||||
const manifest = readJson<DatasetManifest>('setup/golden-dataset/dataset.json');
|
||||
|
||||
assertFileExists('setup/golden-dataset/README.md');
|
||||
for (const fileName of manifest.files) {
|
||||
assertFileExists(`setup/golden-dataset/${fileName}`);
|
||||
assertHeadersMatchTemplate(fileName);
|
||||
}
|
||||
});
|
||||
|
||||
test('golden dataset satisfies required record counts', () => {
|
||||
const manifest = readJson<DatasetManifest>('setup/golden-dataset/dataset.json');
|
||||
|
||||
assert.equal(countRows('setup/golden-dataset/organizations.csv'), manifest.counts.organizations);
|
||||
assert.equal(countRows('setup/golden-dataset/users.csv'), manifest.counts.users);
|
||||
assert.equal(countRows('setup/golden-dataset/customers.csv'), manifest.counts.customers);
|
||||
assert.equal(countRows('setup/golden-dataset/contacts.csv'), manifest.counts.contacts);
|
||||
assert.equal(countRows('setup/golden-dataset/leads.csv'), manifest.counts.leads);
|
||||
assert.equal(countRows('setup/golden-dataset/opportunities.csv'), manifest.counts.opportunities);
|
||||
assert.equal(countRows('setup/golden-dataset/quotations.csv'), manifest.counts.quotations);
|
||||
assert.equal(countRows('setup/golden-dataset/branches.csv'), manifest.counts.branches);
|
||||
assert.equal(countRows('setup/golden-dataset/product-types.csv'), manifest.counts.productTypes);
|
||||
});
|
||||
|
||||
test('golden quotation statuses cover setup approval and revision scenarios', () => {
|
||||
const manifest = readJson<DatasetManifest>('setup/golden-dataset/dataset.json');
|
||||
const quotations = parseCsv('setup/golden-dataset/quotations.csv');
|
||||
const statusIndex = quotations.header.indexOf('status');
|
||||
const statuses = new Set(quotations.rows.map((row) => row[statusIndex]));
|
||||
|
||||
for (const status of manifest.expectedQuotationStatuses) {
|
||||
assert.equal(statuses.has(status), true, `quotation status ${status} should be present`);
|
||||
}
|
||||
});
|
||||
48
src/tests/setup/helpers.ts
Normal file
48
src/tests/setup/helpers.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
export const repoRoot = process.cwd();
|
||||
export const goldenDatasetDir = path.join(repoRoot, 'setup', 'golden-dataset');
|
||||
export const templatesDir = path.join(repoRoot, 'setup', 'templates');
|
||||
|
||||
export interface CsvFile {
|
||||
header: string[];
|
||||
rows: string[][];
|
||||
}
|
||||
|
||||
export function readJson<T>(relativePath: string): T {
|
||||
return JSON.parse(readFileSync(path.join(repoRoot, relativePath), 'utf8')) as T;
|
||||
}
|
||||
|
||||
export function readText(relativePath: string) {
|
||||
return readFileSync(path.join(repoRoot, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
export function assertFileExists(relativePath: string) {
|
||||
assert.equal(existsSync(path.join(repoRoot, relativePath)), true, `${relativePath} should exist`);
|
||||
}
|
||||
|
||||
export function parseCsv(relativePath: string): CsvFile {
|
||||
const content = readText(relativePath).trim();
|
||||
const [headerLine, ...rowLines] = content.split(/\r?\n/);
|
||||
return {
|
||||
header: headerLine.split(','),
|
||||
rows: rowLines.filter(Boolean).map((line) => line.split(','))
|
||||
};
|
||||
}
|
||||
|
||||
export function countRows(relativePath: string) {
|
||||
return parseCsv(relativePath).rows.length;
|
||||
}
|
||||
|
||||
export function sha256(content: string) {
|
||||
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
||||
}
|
||||
|
||||
export function assertHeadersMatchTemplate(fileName: string) {
|
||||
const template = parseCsv(`setup/templates/${fileName}`);
|
||||
const golden = parseCsv(`setup/golden-dataset/${fileName}`);
|
||||
assert.deepEqual(golden.header, template.header, `${fileName} header should match setup template`);
|
||||
}
|
||||
19
src/tests/setup/setup-commit.test.ts
Normal file
19
src/tests/setup/setup-commit.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { assertFileExists, readText } from './helpers.ts';
|
||||
|
||||
test('CSV commit API and seed manifest integration exist', () => {
|
||||
assertFileExists('src/app/api/setup/import/commit/route.ts');
|
||||
assertFileExists('src/features/setup/server/seed-manifest.service.ts');
|
||||
|
||||
const route = readText('src/app/api/setup/import/commit/route.ts');
|
||||
assert.match(route, /createCsvImportSeedManifest/);
|
||||
assert.match(route, /!dryRun/);
|
||||
});
|
||||
|
||||
test('commit engine keeps preview hash validation contract', () => {
|
||||
const service = readText('src/features/setup/server/csv-commit.service.ts');
|
||||
|
||||
assert.match(service, /PREVIEW_HASH_REQUIRED/);
|
||||
assert.match(service, /PREVIEW_HASH_MISMATCH/);
|
||||
});
|
||||
21
src/tests/setup/setup-install.test.ts
Normal file
21
src/tests/setup/setup-install.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { readJson, readText } from './helpers.ts';
|
||||
|
||||
interface DatasetManifest {
|
||||
workflow: string[];
|
||||
}
|
||||
|
||||
test('golden setup workflow includes required platform stages', () => {
|
||||
const manifest = readJson<DatasetManifest>('setup/golden-dataset/dataset.json');
|
||||
|
||||
for (const stage of ['migration', 'readiness', 'template_registry', 'preview', 'commit', 'verification', 'manifest', 'resume', 'reset']) {
|
||||
assert.equal(manifest.workflow.includes(stage), true, `${stage} should be in setup workflow`);
|
||||
}
|
||||
});
|
||||
|
||||
test('integration scenarios prohibit manual SQL as happy path', () => {
|
||||
const scenarios = readText('docs/setup/integration-scenarios.md');
|
||||
|
||||
assert.match(scenarios, /without manual SQL/i);
|
||||
});
|
||||
37
src/tests/setup/setup-preview.test.ts
Normal file
37
src/tests/setup/setup-preview.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { readJson, readText, sha256 } from './helpers.ts';
|
||||
|
||||
interface TemplateRegistry {
|
||||
templates: Array<{ file: string; supported: boolean; importOrder: number }>;
|
||||
}
|
||||
|
||||
interface DatasetManifest {
|
||||
files: string[];
|
||||
}
|
||||
|
||||
test('golden dataset includes every supported setup template', () => {
|
||||
const registry = readJson<TemplateRegistry>('setup/templates/templates.json');
|
||||
const dataset = readJson<DatasetManifest>('setup/golden-dataset/dataset.json');
|
||||
const datasetFiles = new Set(dataset.files);
|
||||
|
||||
for (const template of registry.templates.filter((item) => item.supported)) {
|
||||
assert.equal(datasetFiles.has(template.file), true, `${template.file} should be in golden dataset`);
|
||||
}
|
||||
});
|
||||
|
||||
test('preview hash semantics change when CSV content changes', () => {
|
||||
const original = readText('setup/golden-dataset/customers.csv');
|
||||
const changed = original.replace('Siam Factory', 'Siam Factory Updated');
|
||||
|
||||
assert.notEqual(sha256(original), sha256(changed));
|
||||
});
|
||||
|
||||
test('golden dataset import order follows template registry ordering', () => {
|
||||
const registry = readJson<TemplateRegistry>('setup/templates/templates.json');
|
||||
const dataset = readJson<DatasetManifest>('setup/golden-dataset/dataset.json');
|
||||
const order = new Map(registry.templates.map((template) => [template.file, template.importOrder]));
|
||||
const sorted = [...dataset.files].toSorted((a, b) => (order.get(a) ?? 9999) - (order.get(b) ?? 9999));
|
||||
|
||||
assert.deepEqual(dataset.files, sorted);
|
||||
});
|
||||
32
src/tests/setup/setup-reset.test.ts
Normal file
32
src/tests/setup/setup-reset.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { assertFileExists, readText } from './helpers.ts';
|
||||
|
||||
test('reset and reseed APIs exist', () => {
|
||||
for (const route of [
|
||||
'src/app/api/setup/reset/preview/route.ts',
|
||||
'src/app/api/setup/reset/execute/route.ts',
|
||||
'src/app/api/setup/reseed/preview/route.ts',
|
||||
'src/app/api/setup/reseed/execute/route.ts'
|
||||
]) {
|
||||
assertFileExists(route);
|
||||
}
|
||||
});
|
||||
|
||||
test('reset guard blocks production and requires preview token semantics', () => {
|
||||
const guards = readText('src/features/setup/server/reset-guards.ts');
|
||||
const reset = readText('src/features/setup/server/reset.service.ts');
|
||||
|
||||
assert.match(guards, /NODE_ENV=production/);
|
||||
assert.match(guards, /production-like database/);
|
||||
assert.match(reset, /validateConfirmationToken/);
|
||||
assert.match(reset, /confirmationPhrase/);
|
||||
});
|
||||
|
||||
test('unsafe reset scopes are intentionally blocked', () => {
|
||||
const reset = readText('src/features/setup/server/reset.service.ts');
|
||||
|
||||
for (const phrase of ['Full reset is not exposed', 'Demo/UAT reset requires', 'CSV import rollback requires']) {
|
||||
assert.match(reset, new RegExp(phrase));
|
||||
}
|
||||
});
|
||||
18
src/tests/setup/setup-resume.test.ts
Normal file
18
src/tests/setup/setup-resume.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { assertFileExists, readText } from './helpers.ts';
|
||||
|
||||
test('setup resume APIs exist', () => {
|
||||
for (const route of ['current', 'start', 'steps', 'complete', 'report']) {
|
||||
assertFileExists(`src/app/api/setup/run/${route}/route.ts`);
|
||||
}
|
||||
assertFileExists('src/features/setup/server/setup-state.service.ts');
|
||||
});
|
||||
|
||||
test('setup wizard persists core step outputs', () => {
|
||||
const wizard = readText('src/features/setup/components/setup-wizard.tsx');
|
||||
|
||||
for (const stepKey of ['readiness', 'csv_preview', 'csv_commit', 'verification']) {
|
||||
assert.match(wizard, new RegExp(stepKey));
|
||||
}
|
||||
});
|
||||
18
src/tests/setup/setup-verification.test.ts
Normal file
18
src/tests/setup/setup-verification.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { assertFileExists, readText } from './helpers.ts';
|
||||
|
||||
test('setup readiness and verification APIs exist', () => {
|
||||
assertFileExists('src/app/api/setup/readiness/route.ts');
|
||||
assertFileExists('src/app/api/setup/verify/route.ts');
|
||||
assertFileExists('src/features/setup/server/readiness.service.ts');
|
||||
assertFileExists('src/features/setup/server/verification.service.ts');
|
||||
});
|
||||
|
||||
test('integration scenarios document operational readiness checks', () => {
|
||||
const scenarios = readText('docs/setup/integration-scenarios.md');
|
||||
|
||||
for (const keyword of ['login', 'CRM dashboard', 'quotation approval', 'PDF', 'seed manifest']) {
|
||||
assert.match(scenarios, new RegExp(keyword, 'i'));
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user