+
+
+
Setup Import Form
+
+ Execute import seed data from the configured folder. Failed imports roll back in the commit transaction.
+
+
+
+
+
-
-
Sample template
-
- setup/templates/{template.file}
-
+
+ {workingMessage ?
: null}
+ {importReport ?
: null}
+
+ {previewStatus === 'WARNING' ? (
+
+ setWarningAcknowledged(checked === true)}
+ />
+
+
+ ) : null}
+
+ {mode === 'public' && tokenRequired ? (
+
+
+ Bootstrap Token
+ Enter the setup token before running protected bootstrap steps.
+
+
+ setBootstrapToken(event.target.value)}
+ placeholder='Bootstrap setup token'
+ aria-label='Bootstrap setup token'
+ />
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+ Final Verification
+ Setup can complete after system-required import steps pass.
+
+
+
+ isStepComplete(stepByKey.get(step.stepKey))).length}/${steps.length}`}
+ />
+
+
+
+
+ {completeMutation.isError ? : null}
+
+
+
);
diff --git a/src/features/setup/server/csv/upsert-executor.ts b/src/features/setup/server/csv/upsert-executor.ts
index 22c7c67..ab67869 100644
--- a/src/features/setup/server/csv/upsert-executor.ts
+++ b/src/features/setup/server/csv/upsert-executor.ts
@@ -1,7 +1,7 @@
import 'server-only';
import { hash } from 'bcryptjs';
-import { randomUUID } from 'node:crypto';
+import { createHash, randomUUID } from 'node:crypto';
import { and, eq, isNull, sql } from 'drizzle-orm';
import {
crmApprovalMatrices,
@@ -34,6 +34,25 @@ import { normalizeEmail, normalizeLower, normalizeOrganizationCode, pipeList } f
type ImportTx = typeof db;
+function deterministicId(input: string): string {
+ const digest = createHash('sha256').update(input).digest('hex');
+ const part3 = `5${digest.slice(13, 16)}`;
+ const part4 = `a${digest.slice(17, 20)}`;
+
+ return [
+ digest.slice(0, 8),
+ digest.slice(8, 12),
+ part3,
+ part4,
+ digest.slice(20, 32)
+ ].join('-');
+}
+
+function deterministicOrganizationId(organizationCode: string): string {
+ const normalized = normalizeOrganizationCode(organizationCode).toLowerCase();
+ return deterministicId(`organization:${normalized}`);
+}
+
export class CsvCommitExecutionError extends Error {
files: ImportFileReport[];
@@ -105,11 +124,14 @@ async function findUserId(tx: ImportTx, email: string): Promise
{
async function findOrganization(tx: ImportTx, organizationCode: string): Promise<{ id: string; slug: string } | null> {
if (!organizationCode) return null;
const normalized = normalizeOrganizationCode(organizationCode);
+ const deterministicId = deterministicOrganizationId(normalized);
const row = await first(
tx
.select({ id: organizations.id, slug: organizations.slug })
.from(organizations)
- .where(sql`lower(${organizations.slug}) = ${normalized.toLowerCase()} or ${organizations.id} = ${normalized}`)
+ .where(
+ sql`lower(${organizations.slug}) = ${normalized.toLowerCase()} or ${organizations.id} = ${deterministicId} or ${organizations.id} = ${normalized}`
+ )
.limit(1)
);
return row ?? null;
@@ -306,7 +328,15 @@ async function upsertOrganization(tx: ImportTx, planRow: CsvCommitPlanRow): Prom
const values = planRow.row.values;
const slug = normalizeLower(values.slug);
const createdBy = await resolveUserId(tx, values.created_by_email);
- const existing = await first(tx.select({ id: organizations.id }).from(organizations).where(eq(organizations.slug, slug)).limit(1));
+ const id = deterministicOrganizationId(values.organization_code);
+ const legacyId = normalizeOrganizationCode(values.organization_code);
+ const existing = await first(
+ tx
+ .select({ id: organizations.id })
+ .from(organizations)
+ .where(sql`${organizations.id} = ${id} or ${organizations.id} = ${legacyId} or ${organizations.slug} = ${slug}`)
+ .limit(1)
+ );
const updateValues = {
name: values.organization_name,
imageUrl: emptyToNull(values.logo_url),
@@ -318,7 +348,6 @@ async function upsertOrganization(tx: ImportTx, planRow: CsvCommitPlanRow): Prom
await tx.update(organizations).set(updateValues).where(eq(organizations.id, existing.id));
return { action: 'update', entityId: existing.id };
}
- const id = normalizeOrganizationCode(values.organization_code);
await tx.insert(organizations).values({ id, slug, ...updateValues });
return { action: 'create', entityId: id };
}
diff --git a/src/features/setup/server/setup-bootstrap.service.ts b/src/features/setup/server/setup-bootstrap.service.ts
index 89ee856..01cfaa7 100644
--- a/src/features/setup/server/setup-bootstrap.service.ts
+++ b/src/features/setup/server/setup-bootstrap.service.ts
@@ -5,6 +5,7 @@ import { memberships, organizations, setupRuns, users } from '@/db/schema';
import { db } from '@/lib/db';
import type { ImportReport } from './csv/import-report';
import type { CsvPreviewFileInput, CsvPreviewResult } from './csv/types';
+import { readConfiguredSetupImportSource, type SetupImportSourceReport } from './setup-import-source.service';
import type { SetupStepStatus } from './setup-state.service';
export const BOOTSTRAP_ACTOR_ID = 'bootstrap-setup';
@@ -137,6 +138,66 @@ export class BootstrapSetupError extends Error {
}
}
+const SENSITIVE_COLUMN_PATTERN = /(password|password_hash|token|secret)/i;
+
+function maskSensitiveValues(values: Record) {
+ return Object.fromEntries(
+ Object.entries(values).map(([key, value]) => [
+ key,
+ SENSITIVE_COLUMN_PATTERN.test(key) && value ? '********' : value
+ ])
+ );
+}
+
+export function sanitizeCsvPreviewForResponse(preview: CsvPreviewResult): CsvPreviewResult {
+ return {
+ ...preview,
+ files: preview.files.map((file) => ({
+ ...file,
+ rows: file.rows.slice(0, 5).map((row) => ({
+ ...row,
+ values: maskSensitiveValues(row.values)
+ }))
+ }))
+ };
+}
+
+export function sanitizeCsvPreviewForPersistence(preview: CsvPreviewResult) {
+ return {
+ summary: preview.summary,
+ generatedAt: preview.generatedAt,
+ previewHash: preview.previewHash,
+ files: preview.files.map((file) => ({
+ fileName: file.fileName,
+ template: file.template,
+ importOrder: file.importOrder,
+ status: file.status,
+ summary: file.summary,
+ errors: file.errors,
+ warnings: file.warnings
+ }))
+ };
+}
+
+export function sanitizeCsvPreviewFileForPersistence(preview: CsvPreviewResult, fileName: string) {
+ const file = preview.files.find((candidate) => candidate.fileName.toLowerCase() === fileName.toLowerCase());
+ if (!file) return sanitizeCsvPreviewForPersistence(preview);
+ return {
+ summary: file.summary,
+ generatedAt: preview.generatedAt,
+ previewHash: preview.previewHash,
+ file: {
+ fileName: file.fileName,
+ template: file.template,
+ importOrder: file.importOrder,
+ status: file.status,
+ summary: file.summary,
+ errors: file.errors,
+ warnings: file.warnings
+ }
+ };
+}
+
export interface BootstrapSetupStatus {
initialized: boolean;
tokenRequired: boolean;
@@ -149,6 +210,7 @@ export interface BootstrapSetupStatus {
requiredFiles: string[];
optionalFiles: string[];
requiredSteps: typeof BOOTSTRAP_REQUIRED_STEPS;
+ importSource: SetupImportSourceReport;
}
async function hasRows(query: Promise>) {
@@ -161,7 +223,7 @@ async function hasRows(query: Promise>) {
}
export async function getBootstrapSetupStatus(): Promise {
- const [completedSetupRun, superAdminUser, organization, adminMembership] = await Promise.all([
+ const [completedSetupRun, superAdminUser, organization, adminMembership, importSource] = await Promise.all([
hasRows(
db
.select({ value: count() })
@@ -180,7 +242,11 @@ export async function getBootstrapSetupStatus(): Promise {
.select({ value: count() })
.from(memberships)
.where(eq(memberships.role, 'admin'))
- )
+ ),
+ readConfiguredSetupImportSource({
+ requiredFiles: BOOTSTRAP_REQUIRED_TEMPLATE_FILES,
+ optionalFiles: BOOTSTRAP_OPTIONAL_TEMPLATE_FILES
+ }).then((result) => result.report)
]);
const checks = {
@@ -196,7 +262,8 @@ export async function getBootstrapSetupStatus(): Promise {
checks,
requiredFiles: [...BOOTSTRAP_REQUIRED_TEMPLATE_FILES],
optionalFiles: [...BOOTSTRAP_OPTIONAL_TEMPLATE_FILES],
- requiredSteps: [...BOOTSTRAP_REQUIRED_STEPS]
+ requiredSteps: [...BOOTSTRAP_REQUIRED_STEPS],
+ importSource
};
}
diff --git a/src/features/setup/server/setup-import-source.service.ts b/src/features/setup/server/setup-import-source.service.ts
new file mode 100644
index 0000000..d8a42d7
--- /dev/null
+++ b/src/features/setup/server/setup-import-source.service.ts
@@ -0,0 +1,183 @@
+import 'server-only';
+
+import { createHash } from 'node:crypto';
+import { readdir, readFile, stat } from 'node:fs/promises';
+import path from 'node:path';
+
+import { getCsvTemplateRegistry } from './csv/template-registry';
+import type { CsvPreviewFileInput } from './csv/types';
+
+const SETUP_IMPORT_ENV_KEY = 'SETUP_IMPORT_DIR';
+const DEFAULT_SETUP_IMPORT_DIR = './setup/ALLA-ONVALLA';
+
+export interface SetupImportSourceFile {
+ fileName: string;
+ kind: 'required' | 'optional';
+ bytes: number;
+ modifiedAt: string;
+ checksum: string;
+ headers: string[];
+}
+
+export interface SetupImportIgnoredFile {
+ fileName: string;
+ reason: string;
+}
+
+export interface SetupImportSourceReport {
+ envKey: typeof SETUP_IMPORT_ENV_KEY;
+ exampleValue: typeof DEFAULT_SETUP_IMPORT_DIR;
+ configuredValue: string | null;
+ configured: boolean;
+ resolvedPath: string | null;
+ exists: boolean;
+ files: SetupImportSourceFile[];
+ missingRequiredFiles: string[];
+ ignoredFiles: SetupImportIgnoredFile[];
+ errors: string[];
+ warnings: string[];
+}
+
+export interface SetupImportSourceReadResult {
+ report: SetupImportSourceReport;
+ files: CsvPreviewFileInput[];
+}
+
+function sha256(bytes: Uint8Array) {
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
+}
+
+function readCsvHeaders(bytes: Uint8Array) {
+ const text = new TextDecoder('utf-8', { fatal: false })
+ .decode(bytes)
+ .replace(/^\uFEFF/, '');
+ const [header = ''] = text.split(/\r?\n/, 1);
+ return header.split(',').map((column) => column.trim()).filter(Boolean);
+}
+
+function resolveConfiguredPath(configuredValue: string) {
+ return path.isAbsolute(configuredValue)
+ ? path.normalize(configuredValue)
+ : path.resolve(process.cwd(), configuredValue);
+}
+
+function createBaseReport(configuredValue: string | null): SetupImportSourceReport {
+ return {
+ envKey: SETUP_IMPORT_ENV_KEY,
+ exampleValue: DEFAULT_SETUP_IMPORT_DIR,
+ configuredValue,
+ configured: Boolean(configuredValue),
+ resolvedPath: configuredValue ? resolveConfiguredPath(configuredValue) : null,
+ exists: false,
+ files: [],
+ missingRequiredFiles: [],
+ ignoredFiles: [],
+ errors: [],
+ warnings: []
+ };
+}
+
+export async function readConfiguredSetupImportSource(input: {
+ requiredFiles: readonly string[];
+ optionalFiles: readonly string[];
+}): Promise {
+ const configuredValue = process.env[SETUP_IMPORT_ENV_KEY]?.trim() || null;
+ const report = createBaseReport(configuredValue);
+
+ if (!configuredValue || !report.resolvedPath) {
+ report.errors.push(
+ `Add ${SETUP_IMPORT_ENV_KEY}=${DEFAULT_SETUP_IMPORT_DIR} to .env and restart the server.`
+ );
+ report.missingRequiredFiles = [...input.requiredFiles];
+ return { report, files: [] };
+ }
+
+ const registry = await getCsvTemplateRegistry();
+ const supportedFiles = new Map(
+ registry.templates
+ .filter((template) => template.supported)
+ .map((template) => [template.file.toLowerCase(), template])
+ );
+ const requiredFiles = new Set(input.requiredFiles.map((fileName) => fileName.toLowerCase()));
+ const discovered = new Set();
+ const files: CsvPreviewFileInput[] = [];
+
+ try {
+ const directoryStat = await stat(report.resolvedPath);
+ if (!directoryStat.isDirectory()) {
+ report.errors.push(`${report.resolvedPath} is not a directory.`);
+ report.missingRequiredFiles = [...input.requiredFiles];
+ return { report, files: [] };
+ }
+
+ report.exists = true;
+ const entries = await readdir(report.resolvedPath, { withFileTypes: true });
+
+ for (const entry of entries) {
+ if (!entry.isFile()) {
+ continue;
+ }
+
+ const fileName = path.basename(entry.name);
+ const normalized = fileName.toLowerCase();
+ const template = supportedFiles.get(normalized);
+
+ if (!template) {
+ report.ignoredFiles.push({
+ fileName,
+ reason: 'File is not a registered supported setup CSV template.'
+ });
+ continue;
+ }
+
+ const filePath = path.join(report.resolvedPath, fileName);
+ const [fileStat, bytes] = await Promise.all([stat(filePath), readFile(filePath)]);
+ const byteArray = new Uint8Array(bytes);
+ const kind = requiredFiles.has(normalized) ? 'required' : 'optional';
+
+ discovered.add(normalized);
+ files.push({ fileName: template.file, bytes: byteArray });
+ report.files.push({
+ fileName: template.file,
+ kind,
+ bytes: byteArray.byteLength,
+ modifiedAt: fileStat.mtime.toISOString(),
+ checksum: sha256(byteArray),
+ headers: readCsvHeaders(byteArray)
+ });
+ }
+ } catch (error) {
+ report.errors.push(
+ error instanceof Error
+ ? `Unable to read ${report.resolvedPath}: ${error.message}`
+ : `Unable to read ${report.resolvedPath}.`
+ );
+ }
+
+ report.missingRequiredFiles = input.requiredFiles.filter(
+ (fileName) => !discovered.has(fileName.toLowerCase())
+ );
+
+ if (report.missingRequiredFiles.length > 0) {
+ report.errors.push(
+ `Missing required setup CSV files: ${report.missingRequiredFiles.join(', ')}.`
+ );
+ }
+
+ if (report.ignoredFiles.length > 0) {
+ report.warnings.push('Some files in the setup import directory are ignored.');
+ }
+
+ files.sort((left, right) => {
+ const leftTemplate = supportedFiles.get(left.fileName.toLowerCase());
+ const rightTemplate = supportedFiles.get(right.fileName.toLowerCase());
+ return (leftTemplate?.importOrder ?? 0) - (rightTemplate?.importOrder ?? 0);
+ });
+ report.files.sort((left, right) => {
+ const leftTemplate = supportedFiles.get(left.fileName.toLowerCase());
+ const rightTemplate = supportedFiles.get(right.fileName.toLowerCase());
+ return (leftTemplate?.importOrder ?? 0) - (rightTemplate?.importOrder ?? 0);
+ });
+
+ return { report, files };
+}
diff --git a/src/features/setup/server/setup-state.service.ts b/src/features/setup/server/setup-state.service.ts
index e6e9e6a..24895c7 100644
--- a/src/features/setup/server/setup-state.service.ts
+++ b/src/features/setup/server/setup-state.service.ts
@@ -39,6 +39,7 @@ export interface SaveSetupStepInput {
output?: Record | null;
errors?: unknown[] | null;
warnings?: unknown[] | null;
+ skipStaleInvalidation?: boolean;
}
const ACTIVE_RUN_STATUSES: SetupRunStatus[] = [
@@ -319,7 +320,7 @@ export async function saveSetupStep(input: SaveSetupStepInput) {
}
});
- if (inputChanged) {
+ if (inputChanged && !input.skipStaleInvalidation) {
const staleSteps = STALE_DEPENDENCIES[input.stepKey] ?? [];
await Promise.all(
staleSteps.map((stepKey) =>
diff --git a/src/tests/setup/helpers.ts b/src/tests/setup/helpers.ts
index 55fba39..6f6def5 100644
--- a/src/tests/setup/helpers.ts
+++ b/src/tests/setup/helpers.ts
@@ -26,7 +26,7 @@ export function assertFileExists(relativePath: string) {
export function parseCsv(relativePath: string): CsvFile {
const content = readText(relativePath).trim();
- const [headerLine, ...rowLines] = content.split(/\r?\n/);
+ const [headerLine = '', ...rowLines] = content.split(/\r?\n/);
return {
header: headerLine.split(','),
rows: rowLines.filter(Boolean).map((line) => line.split(','))
diff --git a/src/tests/setup/setup-bootstrap.test.ts b/src/tests/setup/setup-bootstrap.test.ts
index 565ec38..682f889 100644
--- a/src/tests/setup/setup-bootstrap.test.ts
+++ b/src/tests/setup/setup-bootstrap.test.ts
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import test from 'node:test';
+
import { assertFileExists, readText } from './helpers.ts';
test('first-run bootstrap setup public surfaces exist', () => {
@@ -8,8 +9,9 @@ test('first-run bootstrap setup public surfaces exist', () => {
}
assertFileExists('src/app/administration/setup/page.tsx');
- assertFileExists('src/features/setup/components/bootstrap-setup.tsx');
+ assertFileExists('src/features/setup/components/setup-wizard.tsx');
assertFileExists('src/features/setup/server/setup-bootstrap.service.ts');
+ assertFileExists('src/features/setup/server/setup-import-source.service.ts');
});
test('bootstrap setup guards public access after initialization', () => {
@@ -29,7 +31,7 @@ test('bootstrap setup guards public access after initialization', () => {
assert.match(proxy, /\/api\/setup\/templates/);
});
-test('bootstrap setup enforces required CSV minimum and preview hash commit', () => {
+test('bootstrap setup supports configured-directory preview and commit', () => {
const service = readText('src/features/setup/server/setup-bootstrap.service.ts');
const commitRoute = readText('src/app/api/setup/bootstrap/commit/route.ts');
const previewRoute = readText('src/app/api/setup/bootstrap/preview/route.ts');
@@ -38,6 +40,7 @@ test('bootstrap setup enforces required CSV minimum and preview hash commit', ()
'users.csv',
'organizations.csv',
'memberships.csv',
+ 'crm-role-assignments.csv',
'master-options.csv',
'branches.csv',
'product-types.csv',
@@ -49,8 +52,16 @@ test('bootstrap setup enforces required CSV minimum and preview hash commit', ()
assert.match(service, new RegExp(fileName.replace('.', '\\.')));
}
- assert.match(previewRoute, /assertBootstrapRequiredFiles/);
+ assert.match(previewRoute, /application\/json/);
+ assert.match(previewRoute, /configured-directory/);
+ assert.match(previewRoute, /readConfiguredSetupImportSource/);
+ assert.match(previewRoute, /saveConfiguredPreviewSteps/);
+
+ assert.match(commitRoute, /application\/json/);
+ assert.match(commitRoute, /configured-directory/);
assert.match(commitRoute, /previewHash/);
assert.match(commitRoute, /commitSetupCsvImport/);
- assert.match(commitRoute, /createCsvImportSeedManifest/);
+ assert.match(commitRoute, /saveConfiguredCommitSteps/);
+ assert.match(commitRoute, /skipStaleInvalidation: true/);
+ assert.match(previewRoute, /skipStaleInvalidation: true/);
});
diff --git a/src/tests/setup/setup-commit.test.ts b/src/tests/setup/setup-commit.test.ts
index 60b2a53..f0d6c37 100644
--- a/src/tests/setup/setup-commit.test.ts
+++ b/src/tests/setup/setup-commit.test.ts
@@ -1,8 +1,9 @@
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', () => {
+test('CSV commit API seed manifest integration exist', () => {
assertFileExists('src/app/api/setup/import/commit/route.ts');
assertFileExists('src/features/setup/server/seed-manifest.service.ts');
@@ -17,3 +18,12 @@ test('commit engine keeps preview hash validation contract', () => {
assert.match(service, /PREVIEW_HASH_REQUIRED/);
assert.match(service, /PREVIEW_HASH_MISMATCH/);
});
+
+test('organization CSV import uses deterministic UUID primary keys', () => {
+ const executor = readText('src/features/setup/server/csv/upsert-executor.ts');
+
+ assert.match(executor, /function deterministicOrganizationId/);
+ assert.match(executor, /deterministicId\(`organization:\$\{normalized\}`\)/);
+ assert.match(executor, /const id = deterministicOrganizationId\(values\.organization_code\)/);
+ assert.doesNotMatch(executor, /const id = normalizeOrganizationCode\(values\.organization_code\)/);
+});
diff --git a/src/tests/setup/setup-import-source.test.ts b/src/tests/setup/setup-import-source.test.ts
new file mode 100644
index 0000000..30903a0
--- /dev/null
+++ b/src/tests/setup/setup-import-source.test.ts
@@ -0,0 +1,66 @@
+import assert from 'node:assert/strict';
+import { existsSync } from 'node:fs';
+import path from 'node:path';
+import test from 'node:test';
+
+import { assertFileExists, readJson, readText, repoRoot } from './helpers.ts';
+
+interface TemplateMetadata {
+ templates: Array<{ file: string; supported: boolean }>;
+}
+
+const requiredFiles = [
+ 'users.csv',
+ 'organizations.csv',
+ 'memberships.csv',
+ 'crm-role-assignments.csv',
+ 'master-options.csv',
+ 'branches.csv',
+ 'product-types.csv',
+ 'document-sequences.csv',
+ 'approval-workflows.csv',
+ 'approval-steps.csv',
+ 'approval-matrix.csv'
+];
+
+test('setup import source service resolves env-based configured folder', () => {
+ const service = readText('src/features/setup/server/setup-import-source.service.ts');
+
+ assert.match(service, /SETUP_IMPORT_DIR/);
+ assert.match(service, /\.\/setup\/ALLA-ONVALLA/);
+ assert.match(service, /process\.cwd\(\)/);
+ assert.match(service, /path\.resolve/);
+ assert.match(service, /missingRequiredFiles/);
+ assert.match(service, /ignoredFiles/);
+ assert.match(service, /checksum/);
+ assert.match(service, /getCsvTemplateRegistry/);
+});
+
+test('sample ALLA setup folder contains required bootstrap CSV files', () => {
+ const folder = path.join(repoRoot, 'setup', 'ALLA-ONVALLA');
+ assert.equal(existsSync(folder), true, 'setup/ALLA-ONVALLA should exist');
+
+ for (const fileName of requiredFiles) {
+ assertFileExists(`setup/ALLA-ONVALLA/${fileName}`);
+ }
+});
+
+test('configured setup import source only reads registered supported templates', () => {
+ const metadata = readJson('setup/templates/templates.json');
+ const supportedFiles = new Set(
+ metadata.templates.filter((template) => template.supported).map((template) => template.file)
+ );
+
+ for (const fileName of requiredFiles) {
+ assert.equal(supportedFiles.has(fileName), true, `${fileName} should be a supported setup template`);
+ }
+
+ const service = readText('src/features/setup/server/setup-import-source.service.ts');
+ assert.match(service, /template\.supported/);
+ assert.match(service, /File is not a registered supported setup CSV template/);
+});
+
+test('environment documents setup import directory', () => {
+ const env = readText('.env');
+ assert.match(env, /SETUP_IMPORT_DIR=\.\/setup\/ALLA-ONVALLA/);
+});
diff --git a/src/tests/setup/setup-plugin-registry.test.ts b/src/tests/setup/setup-plugin-registry.test.ts
index 3819c87..3a57a40 100644
--- a/src/tests/setup/setup-plugin-registry.test.ts
+++ b/src/tests/setup/setup-plugin-registry.test.ts
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import test from 'node:test';
+
import { assertFileExists, readText } from './helpers.ts';
test('setup plugin registry files exist', () => {
@@ -15,12 +16,13 @@ test('setup plugin registry files exist', () => {
}
});
-test('installation profile API and wizard profile loading exist', () => {
+test('installation profile API remains available while wizard uses configured import source', () => {
assertFileExists('src/app/api/setup/profiles/route.ts');
const wizard = readText('src/features/setup/components/setup-wizard.tsx');
- assert.match(wizard, /setupProfilesQueryOptions/);
- assert.match(wizard, /Installation profile/);
+ assert.match(wizard, /setupTemplatesQueryOptions/);
+ assert.match(wizard, /Configured Import Folder/);
+ assert.match(wizard, /source\.envKey/);
});
test('template API exposes plugin-aware metadata without removing legacy fields', () => {
diff --git a/src/tests/setup/setup-resume.test.ts b/src/tests/setup/setup-resume.test.ts
index dfae82d..0afbf36 100644
--- a/src/tests/setup/setup-resume.test.ts
+++ b/src/tests/setup/setup-resume.test.ts
@@ -1,18 +1,23 @@
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/app/api/setup/bootstrap/run/current/route.ts');
assertFileExists('src/features/setup/server/setup-state.service.ts');
});
-test('setup wizard persists core step outputs', () => {
+test('setup wizard refreshes run state after configured-folder import', () => {
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));
- }
+ assert.match(wizard, /useConfiguredBootstrapCsvPreview/);
+ assert.match(wizard, /useConfiguredBootstrapCsvCommit/);
+ assert.match(wizard, /setupRunQuery\.refetch/);
+ assert.match(wizard, /bootstrapStatusQuery\.refetch/);
+ assert.match(wizard, /Mark setup completed/);
});