task-d.7.10

This commit is contained in:
phaichayon
2026-07-03 11:23:24 +07:00
parent ccfd7c6cd7
commit 503f34e22f
37 changed files with 1511 additions and 0 deletions

View 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`);
}
});

View 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`);
}

View 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/);
});

View 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);
});

View 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);
});

View 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));
}
});

View 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));
}
});

View 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'));
}
});