Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/ui/.eslintignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
**/node_modules/*
**/out/*
**/public/docs/*
**/scripts/*
**/.next/*
22 changes: 22 additions & 0 deletions apps/ui/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,28 @@
{
"allow": ["warn", "error"]
}
],
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"../features/*/*",
"!../features/*/index",
"!../features/*/index.ts",
"!../features/*/index.tsx",
"@/src/features/*/*",
"!@/src/features/*/index",
"!@/src/features/*/index.ts",
"!@/src/features/*/index.tsx",
"../features/*/internal/*",
"@/src/features/*/internal/*"
],
"message": "Cross-feature deep imports are rejected. Import features only via their public index.ts."
}
]
}
]
},
"overrides": [
Expand Down
10 changes: 10 additions & 0 deletions apps/ui/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
# UI

## Purpose

Provides the Next.js frontend for Holiday Peak Hub operations and workflows.

## Responsibilities

- Render admin and operations interfaces for retail workflows.
- Call backend APIs for transactional and intelligence scenarios.
- Provide a single web entry point for platform users.

## Key endpoints or interfaces

- Next.js app interface served through the standard web root.
- API integration targets are configured through frontend environment variables.

## Run/Test commands

```bash
yarn --cwd apps/ui install
yarn --cwd apps/ui dev
Expand All @@ -24,12 +28,18 @@ yarn --cwd apps/ui type-check
```

## Coverage and quality gates

- Jest enforces global coverage thresholds (`branches/functions/lines/statements >= 60%`) in `apps/ui/jest.config.js`.
- Baseline critical-flow E2E coverage runs through Playwright (`apps/ui/tests/e2e/critical-flows.spec.ts`).
- Executive demo and operator regression coverage now also includes `apps/ui/tests/e2e/demo-narrative.spec.ts`, `apps/ui/tests/e2e/dark-mode-regression.spec.ts`, and `apps/ui/tests/e2e/cockpit-readiness.spec.ts`.
- Focused unit coverage also validates telemetry persistence for enrichment-backed product loads and graph-summary enrichment calls.

## Feature import boundaries

- Cross-feature deep imports are rejected by ESLint. Import features only via their public `index.ts`.

## Configuration notes

- Uses frontend environment variables for backend/API URLs and auth integration.
- Build and runtime behavior are controlled by `apps/ui/package.json` scripts.
- Uses Next.js and TypeScript toolchain configured in the UI app directory.
Expand Down
142 changes: 142 additions & 0 deletions apps/ui/tests/unit/featureIsolationLint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* Tests for the feature-isolation ESLint contract (Issue #991).
*
* No GoF runtime pattern applies; this is a governance contract test that
* pins ESLint configuration data for ADR-033's modular-monolith boundary.
*/

import { existsSync, readFileSync } from 'fs';
import { resolve } from 'path';
import { deserialize, serialize } from 'v8';
import { ESLint } from 'eslint';

const APP_ROOT = resolve(__dirname, '..', '..');
const SYNTHETIC_FILE_PATH = resolve(APP_ROOT, 'src', 'app', 'synthetic-feature-isolation.tsx');
const FEATURE_ISOLATION_MESSAGE =
'Cross-feature deep imports are rejected. Import features only via their public index.ts.';

interface RestrictedImportPattern {
group?: string[];
message?: string;
}

interface EslintConfig {
rules?: Record<string, unknown>;
}

const isRestrictedImportPattern = (value: unknown): value is RestrictedImportPattern => {
if (typeof value !== 'object' || value === null) {
return false;
}

const candidate = value as { group?: unknown; message?: unknown };
return Array.isArray(candidate.group) && typeof candidate.message === 'string';
};

const ensureStructuredClone = () => {
if (typeof globalThis.structuredClone === 'function') {
return;
}

Object.defineProperty(globalThis, 'structuredClone', {
configurable: true,
value: <Value>(value: Value): Value => deserialize(serialize(value)) as Value,
});
};

const lintSyntheticSource = async (source: string) => {
ensureStructuredClone();

const eslint = new ESLint({
cwd: APP_ROOT,
overrideConfigFile: resolve(APP_ROOT, '.eslintrc.json'),
});

const [result] = await eslint.lintText(source, {
filePath: SYNTHETIC_FILE_PATH,
warnIgnored: false,
});

return result.messages;
};

describe('feature-isolation ESLint contract (Issue #991)', () => {
const config = (() => {
const path = resolve(APP_ROOT, '.eslintrc.json');
expect(existsSync(path)).toBe(true);
return JSON.parse(readFileSync(path, 'utf-8')) as EslintConfig;
})();

const rule = config.rules?.['no-restricted-imports'];

it('defines a root no-restricted-imports rule', () => {
expect(Array.isArray(rule)).toBe(true);
expect((rule as unknown[])[0]).toBe('error');
});

it('rejects cross-feature deep imports while allowing public index surfaces', () => {
expect(Array.isArray(rule)).toBe(true);

const [, options] = rule as [string, { patterns?: unknown[] }];
const featurePattern = options.patterns?.find(isRestrictedImportPattern);

expect(featurePattern).toBeDefined();
expect(featurePattern?.group).toEqual(
expect.arrayContaining([
'../features/*/*',
'!../features/*/index',
'!../features/*/index.ts',
'!../features/*/index.tsx',
'@/src/features/*/*',
'!@/src/features/*/index',
'!@/src/features/*/index.ts',
'!@/src/features/*/index.tsx',
'../features/*/internal/*',
'@/src/features/*/internal/*',
]),
);
});

it('explains that features must be imported through public index.ts', () => {
expect(Array.isArray(rule)).toBe(true);

const [, options] = rule as [string, { patterns?: unknown[] }];
const featurePattern = options.patterns?.find(isRestrictedImportPattern);

expect(featurePattern?.message).toContain('Cross-feature deep imports are rejected');
expect(featurePattern?.message).toContain('public index.ts');
});

it('reports the feature-isolation rule for synthetic cross-feature deep imports', async () => {
const messages = await lintSyntheticSource(`
import { calculateInventoryRisk } from '@/src/features/inventory/internal/risk';

export const selectedRiskCalculator = calculateInventoryRisk;
`);

expect(messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
ruleId: 'no-restricted-imports',
message: expect.stringContaining(FEATURE_ISOLATION_MESSAGE),
}),
]),
);
});

it('allows synthetic public feature index imports', async () => {
const messages = await lintSyntheticSource(`
import { InventoryFeature } from '@/src/features/inventory/index';

export const selectedFeature = InventoryFeature;
`);

expect(messages).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
ruleId: 'no-restricted-imports',
}),
]),
);
});
});
Loading