mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Align release header audit with publish contract
This commit is contained in:
@@ -168,6 +168,9 @@ jobs:
|
||||
- name: Lint frontend
|
||||
run: npm --prefix frontend-modern run lint
|
||||
|
||||
- name: Audit header composition
|
||||
run: npm --prefix frontend-modern run lint:headers
|
||||
|
||||
- name: Check frontend copy-paste duplication
|
||||
run: npm --prefix frontend-modern run lint:cpd
|
||||
|
||||
|
||||
@@ -241,6 +241,15 @@ ledger reads until that policy resolves and suppress them while commercial
|
||||
surfaces are hidden. Public demo mode therefore renders a redacted
|
||||
presentation-policy state instead of creating a fake entitlement, probing
|
||||
hidden commercial endpoints, or showing monitored-system usage pressure.
|
||||
That same commercial/public browser boundary also owns pricing-handoff
|
||||
framing. `frontend-modern/src/pages/PricingHandoff.tsx` may keep the
|
||||
operator-visible handoff on `/pricing`, but it must render the shared
|
||||
`PageHeader` shell while `frontend-modern/src/utils/pricingHandoff.ts`
|
||||
continues to own destination resolution, self-hosted Pulse Account handoff,
|
||||
and public-pricing fallback truth. The route must not fork a raw top-level
|
||||
heading, duplicate destination logic in the page shell, or let commercial
|
||||
handoff framing drift away from the same shared browser chrome used by the
|
||||
rest of the product.
|
||||
The governed browser proof for that posture lives in
|
||||
`tests/integration/tests/53-demo-mode-commercial-boundary.spec.ts` and is
|
||||
expected to stay runnable through
|
||||
|
||||
@@ -186,6 +186,11 @@ state. When `.github/workflows/create-release.yml` runs in `draft_only` mode,
|
||||
it must pass the real draft state into `.github/workflows/validate-release-assets.yml`
|
||||
so validation blocks or annotates the draft release as a draft, rather than
|
||||
misclassifying the run as post-publish revalidation.
|
||||
That same frontend-release boundary also owns shared header-composition proof.
|
||||
`.github/workflows/release-dry-run.yml` and `.github/workflows/create-release.yml`
|
||||
must both run the same `lint:headers` audit so a branch that would be rejected
|
||||
by the real publish workflow cannot pass the governed dry run only because the
|
||||
rehearsal skipped that header-composition gate.
|
||||
That same governed demo-deployment boundary now owns target separation between
|
||||
the public stable demo and the opt-in v6 preview demo. `.github/workflows/create-release.yml`,
|
||||
`.github/workflows/update-demo-server.yml`, and `.github/workflows/deploy-demo-server.yml`
|
||||
|
||||
@@ -373,6 +373,17 @@ connections` visible as the API-backed alternative for Proxmox and
|
||||
such as `truenas_checked` instead of letting feature-local fixtures or
|
||||
fallback objects collapse API-backed TrueNAS systems back into generic
|
||||
agent-host presentation.
|
||||
That same shared route-shell boundary also owns header-composition audit.
|
||||
`frontend-modern/scripts/header-audit.mjs`,
|
||||
`.github/workflows/release-dry-run.yml`, and
|
||||
`.github/workflows/create-release.yml` must prove the same shared
|
||||
top-level page-header contract before publication. The audit may follow
|
||||
local imports when a route shell composes `PageHeader` through a nested
|
||||
surface, and settings coverage must stay limited to top-level registry
|
||||
panels rather than every helper `*Panel.tsx` file. Route shells such as
|
||||
`frontend-modern/src/features/operations/OperationsPageSurface.tsx` must
|
||||
therefore keep the shared `PageHeader` above owned subtabs instead of
|
||||
drifting back to page-local `<h1>` framing.
|
||||
23. Keep the authenticated app root aligned with that same first-session path.
|
||||
That same shared-primitive ownership now includes contextual row focus.
|
||||
`frontend-modern/src/components/shared/contextualFocus.ts` is the canonical
|
||||
|
||||
@@ -236,6 +236,14 @@ regression protection.
|
||||
must not hydrate `all-resources` or recovery rollups behind a hidden node
|
||||
summary; selector-owned data hooks must be explicitly visibility-gated so
|
||||
`/workloads` only pays for workload-owned transports.
|
||||
34. Keep dashboard page-header framing additive on the compact hot path.
|
||||
`frontend-modern/src/pages/Dashboard.tsx` may render the shared
|
||||
`PageHeader` for route-level shell consistency, but that header must stay
|
||||
pure presentation on top of the existing compact overview, trends,
|
||||
actions, and recovery/storage widget hydration. It must not introduce a
|
||||
second dashboard data load, widen suspense ownership, or force dashboard
|
||||
summaries back through full-resource fetch paths just to satisfy page
|
||||
chrome.
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
||||
@@ -292,6 +292,13 @@ querying, and the operator-facing storage health presentation layer.
|
||||
`/api/storage-charts` payload once per additional dashboard resource page
|
||||
or invent a dashboard-only storage summary transport path outside the
|
||||
canonical cache owners.
|
||||
38. Keep storage and recovery route framing additive and owner-neutral.
|
||||
`frontend-modern/src/components/Storage/Storage.tsx` and storage/recovery-
|
||||
adjacent dashboard composition may use the shared `PageHeader` shell for
|
||||
top-level route framing, but that header must stay additive on top of the
|
||||
canonical storage page model, recovery presenters, and shared summary
|
||||
caches. Header chrome must not become a second owner for storage filters,
|
||||
recovery posture, commercial purchase state, or transport selection.
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
||||
@@ -337,6 +337,13 @@ assembly branch.
|
||||
compact `/api/charts/storage-summary` contract instead of rebuilding
|
||||
page-local per-resource storage history fetches, storage-type aliases, or
|
||||
full storage-page `/api/storage-charts` fetches.
|
||||
19. Keep infrastructure page-header framing presentation-only on the page
|
||||
shell. `frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx`
|
||||
may render the shared `PageHeader`, but canonical source/status/search
|
||||
state, summary scope, and row selection must remain on
|
||||
`frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts`
|
||||
and the unified-resource selectors it composes. The header must not become
|
||||
a second state owner, scope banner, or provider-local filter surface.
|
||||
|
||||
## Current State
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ const REQUIRED_PAGE_HEADERS = new Map([
|
||||
['src/pages/Infrastructure.tsx', 'PageHeader'],
|
||||
['src/pages/Operations.tsx', 'PageHeader'],
|
||||
['src/pages/NotFound.tsx', 'PageHeader'],
|
||||
['src/pages/Pricing.tsx', 'PageHeader'],
|
||||
['src/pages/MigrationGuide.tsx', 'PageHeader'],
|
||||
['src/pages/PricingHandoff.tsx', 'PageHeader'],
|
||||
['src/components/Settings/Settings.tsx', 'PageHeader'],
|
||||
]);
|
||||
|
||||
@@ -41,6 +40,75 @@ function hasPrimitive(content, primitive) {
|
||||
return new RegExp(`<${primitive}\\b`).test(content);
|
||||
}
|
||||
|
||||
function resolveImport(specifier, fromFile) {
|
||||
let basePath = null;
|
||||
if (specifier.startsWith('@/')) {
|
||||
basePath = path.join(ROOT, 'src', specifier.slice(2));
|
||||
} else if (specifier.startsWith('.')) {
|
||||
basePath = path.resolve(path.dirname(path.join(ROOT, fromFile)), specifier);
|
||||
}
|
||||
|
||||
if (!basePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
basePath,
|
||||
`${basePath}.tsx`,
|
||||
`${basePath}.ts`,
|
||||
path.join(basePath, 'index.tsx'),
|
||||
path.join(basePath, 'index.ts'),
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
||||
return path.relative(ROOT, candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getImportedLocalFiles(relPath) {
|
||||
const content = readFileSafe(relPath);
|
||||
if (!content) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const imports = new Set();
|
||||
const importPattern = /from\s+['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\)/g;
|
||||
let match;
|
||||
while ((match = importPattern.exec(content)) !== null) {
|
||||
const specifier = match[1] ?? match[2];
|
||||
const resolved = resolveImport(specifier, relPath);
|
||||
if (resolved) {
|
||||
imports.add(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(imports);
|
||||
}
|
||||
|
||||
function collectPrimitiveUsage(relPath, visited = new Set()) {
|
||||
if (visited.has(relPath)) {
|
||||
return new Set();
|
||||
}
|
||||
visited.add(relPath);
|
||||
|
||||
const content = readFileSafe(relPath);
|
||||
const primitives = new Set(
|
||||
HEADER_PRIMITIVES.filter((primitive) => hasPrimitive(content, primitive)),
|
||||
);
|
||||
|
||||
for (const importedFile of getImportedLocalFiles(relPath)) {
|
||||
for (const primitive of collectPrimitiveUsage(importedFile, visited)) {
|
||||
primitives.add(primitive);
|
||||
}
|
||||
}
|
||||
|
||||
return primitives;
|
||||
}
|
||||
|
||||
function listTopLevelPages() {
|
||||
const dir = path.join(ROOT, 'src/pages');
|
||||
return fs
|
||||
@@ -50,13 +118,20 @@ function listTopLevelPages() {
|
||||
.sort();
|
||||
}
|
||||
|
||||
function listSettingsPanels() {
|
||||
const dir = path.join(ROOT, 'src/components/Settings');
|
||||
return fs
|
||||
.readdirSync(dir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('Panel.tsx'))
|
||||
.map((entry) => `src/components/Settings/${entry.name}`)
|
||||
.sort();
|
||||
function listTopLevelSettingsPanels() {
|
||||
const registryFile = 'src/components/Settings/settingsPanelRegistryLoaders.ts';
|
||||
const content = readFileSafe(registryFile);
|
||||
const panels = new Set();
|
||||
const importPattern = /import\('\.\/([^']+)'\)/g;
|
||||
let match;
|
||||
while ((match = importPattern.exec(content)) !== null) {
|
||||
const moduleName = match[1];
|
||||
if (!moduleName.endsWith('Panel')) {
|
||||
continue;
|
||||
}
|
||||
panels.add(`src/components/Settings/${moduleName}.tsx`);
|
||||
}
|
||||
return Array.from(panels).sort();
|
||||
}
|
||||
|
||||
const failures = [];
|
||||
@@ -67,7 +142,8 @@ for (const [file, requiredPrimitive] of REQUIRED_PAGE_HEADERS.entries()) {
|
||||
failures.push(`${file}: missing file`);
|
||||
continue;
|
||||
}
|
||||
if (!hasPrimitive(content, requiredPrimitive)) {
|
||||
const primitives = collectPrimitiveUsage(file);
|
||||
if (!primitives.has(requiredPrimitive)) {
|
||||
failures.push(`${file}: must use <${requiredPrimitive}>`);
|
||||
}
|
||||
}
|
||||
@@ -86,7 +162,7 @@ for (const [file, requiredPrimitive] of REQUIRED_OPERATIONS_WRAPPERS.entries())
|
||||
const pageInventory = [];
|
||||
for (const pageFile of listTopLevelPages()) {
|
||||
const content = readFileSafe(pageFile);
|
||||
const primitives = HEADER_PRIMITIVES.filter((primitive) => hasPrimitive(content, primitive));
|
||||
const primitives = Array.from(collectPrimitiveUsage(pageFile));
|
||||
const hasRawH1 = /<h1\b/.test(content);
|
||||
|
||||
pageInventory.push({
|
||||
@@ -108,13 +184,11 @@ for (const pageFile of listTopLevelPages()) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const panelFile of listSettingsPanels()) {
|
||||
if (panelFile === 'src/components/Settings/OperationsPanel.tsx') continue;
|
||||
for (const panelFile of listTopLevelSettingsPanels()) {
|
||||
if (SETTINGS_PANEL_SHIMS.has(panelFile)) continue;
|
||||
|
||||
const content = readFileSafe(panelFile);
|
||||
const hasSharedWrapper =
|
||||
hasPrimitive(content, 'SettingsPanel') || hasPrimitive(content, 'OperationsPanel');
|
||||
const primitives = collectPrimitiveUsage(panelFile);
|
||||
const hasSharedWrapper = primitives.has('SettingsPanel') || primitives.has('OperationsPanel');
|
||||
|
||||
if (!hasSharedWrapper) {
|
||||
failures.push(`${panelFile}: must use <SettingsPanel> or <OperationsPanel>`);
|
||||
|
||||
@@ -4,6 +4,7 @@ import StorageContentCard from '@/components/Storage/StorageContentCard';
|
||||
import StoragePageBanners from '@/components/Storage/StoragePageBanners';
|
||||
import StoragePageControls from '@/components/Storage/StoragePageControls';
|
||||
import StoragePageSummary from '@/components/Storage/StoragePageSummary';
|
||||
import { PageHeader } from '@/components/shared/PageHeader';
|
||||
import { StickySummarySection } from '@/components/shared/StickySummarySection';
|
||||
import { isStorageRecordCeph } from './storagePageState';
|
||||
import { useStoragePageModel } from './useStoragePageModel';
|
||||
@@ -73,6 +74,11 @@ const Storage: Component = () => {
|
||||
class="space-y-4"
|
||||
data-testid="storage-page"
|
||||
>
|
||||
<PageHeader
|
||||
title="Storage"
|
||||
description="Track capacity, cluster health, and storage alerts across local and distributed systems."
|
||||
/>
|
||||
|
||||
<StickySummarySection desktopOnly={false}>
|
||||
<StoragePageSummary
|
||||
filteredRecordCount={() => filteredRecords().length}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { buildInfrastructureWorkspacePath } from '@/components/Settings/infrastr
|
||||
import { EmptyState } from '@/components/shared/EmptyState';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { FilterSegmentedControl, LabeledFilterSelect } from '@/components/shared/FilterToolbar';
|
||||
import { PageHeader } from '@/components/shared/PageHeader';
|
||||
import { PageControls } from '@/components/shared/PageControls';
|
||||
import { SearchInput } from '@/components/shared/SearchInput';
|
||||
import { StickySummarySection } from '@/components/shared/StickySummarySection';
|
||||
@@ -86,6 +87,11 @@ export function InfrastructurePageSurface() {
|
||||
data-testid="infrastructure-page"
|
||||
class="space-y-4"
|
||||
>
|
||||
<PageHeader
|
||||
title="Infrastructure"
|
||||
description="Inspect discovered systems, cluster health, and resource status across the monitored estate."
|
||||
/>
|
||||
|
||||
<Show
|
||||
when={!loading() || initialLoadComplete()}
|
||||
fallback={
|
||||
|
||||
+3
@@ -11,6 +11,9 @@ describe('InfrastructurePageSurface guardrails', () => {
|
||||
it('keeps the feature shell separate from route-sync and page-model ownership', () => {
|
||||
expect(infrastructurePageSurfaceSource).toContain('useInfrastructurePageState');
|
||||
expect(infrastructurePageSurfaceSource).toContain('useNavigate');
|
||||
expect(infrastructurePageSurfaceSource).toContain("import { PageHeader } from '@/components/shared/PageHeader';");
|
||||
expect(infrastructurePageSurfaceSource).toContain('<PageHeader');
|
||||
expect(infrastructurePageSurfaceSource).toContain('title="Infrastructure"');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('useLocation(');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('buildInfrastructurePath(');
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import TerminalIcon from 'lucide-solid/icons/terminal';
|
||||
import { DiagnosticsPanel } from '@/components/Settings/DiagnosticsPanel';
|
||||
import { ReportingPanel } from '@/components/Settings/ReportingPanel';
|
||||
import { SystemLogsPanel } from '@/components/Settings/SystemLogsPanel';
|
||||
import { PageHeader } from '@/components/shared/PageHeader';
|
||||
import { Subtabs, type SubtabOption } from '@/components/shared/Subtabs';
|
||||
import { DASHBOARD_PATH } from '@/routing/resourceLinks';
|
||||
import { presentationPolicyIsDemoMode } from '@/stores/sessionPresentationPolicy';
|
||||
@@ -62,6 +63,11 @@ export function OperationsPageSurface() {
|
||||
return (
|
||||
<Show when={!hiddenInDemoMode()}>
|
||||
<div class="space-y-6">
|
||||
<PageHeader
|
||||
title="Operations"
|
||||
description="Run diagnostics, review generated reports, and inspect system logs without leaving the app."
|
||||
/>
|
||||
|
||||
<div class="mb-6">
|
||||
<Subtabs
|
||||
value={activeTab()}
|
||||
|
||||
@@ -34,6 +34,7 @@ import { RecentAlertsPanel } from '@/components/Alerts/RecentAlertsPanel';
|
||||
import { RelayOnboardingCard } from '@/components/Dashboard/RelayOnboardingCard';
|
||||
import { DashboardRecoveryStatusPanel } from '@/components/Recovery/DashboardRecoveryStatusPanel';
|
||||
import { DashboardStoragePanel } from '@/components/Storage/DashboardStoragePanel';
|
||||
import { PageHeader } from '@/components/shared/PageHeader';
|
||||
import type { DashboardWidgetDef, DashboardWidgetId } from '@/features/dashboardOverview/dashboardWidgets';
|
||||
export default function Dashboard() {
|
||||
const navigate = useNavigate();
|
||||
@@ -169,6 +170,11 @@ export default function Dashboard() {
|
||||
|
||||
return (
|
||||
<main data-testid="dashboard-page" class="space-y-6">
|
||||
<PageHeader
|
||||
title="Dashboard"
|
||||
description="Monitor fleet health, recent alerts, storage pressure, and recovery readiness from one surface."
|
||||
/>
|
||||
|
||||
{/* Connection warning banner — shown above all content, NOT a full-page takeover */}
|
||||
<Show when={hasConnectionError() && initialLoadComplete()}>
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Navigate, useLocation } from '@solidjs/router';
|
||||
import { Show, createMemo, onMount } from 'solid-js';
|
||||
import { PageHeader } from '@/components/shared/PageHeader';
|
||||
import { trackPaywallViewed } from '@/utils/upgradeMetrics';
|
||||
import {
|
||||
getPricingRouteDestination,
|
||||
@@ -36,17 +37,22 @@ export default function PricingHandoff() {
|
||||
fallback={<Navigate href={destination()} />}
|
||||
>
|
||||
<div class="flex min-h-[50vh] items-center justify-center">
|
||||
<div class="space-y-2 text-center">
|
||||
<h1 class="text-lg font-semibold text-base-content">
|
||||
Redirecting to {handoffLabel()}
|
||||
</h1>
|
||||
<p class="text-sm text-muted">
|
||||
If the handoff does not start automatically,{' '}
|
||||
<a href={destination()} class="text-blue-600 hover:underline dark:text-blue-400">
|
||||
{handoffLinkLabel()}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div class="max-w-xl space-y-2 text-center">
|
||||
<PageHeader
|
||||
title={`Redirecting to ${handoffLabel()}`}
|
||||
description={
|
||||
<>
|
||||
If the handoff does not start automatically,{' '}
|
||||
<a href={destination()} class="text-blue-600 hover:underline dark:text-blue-400">
|
||||
{handoffLinkLabel()}
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
}
|
||||
class="items-center text-center"
|
||||
titleClass="text-lg"
|
||||
descriptionClass="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -146,6 +146,9 @@ describe('Dashboard page module contract', () => {
|
||||
it('routes dashboard overview panels through the dashboard overview feature owner', () => {
|
||||
expect(dashboardPageSource).toContain("from '@/features/dashboardOverview'");
|
||||
expect(dashboardPageSource).toContain("from '@/components/Dashboard/RelayOnboardingCard'");
|
||||
expect(dashboardPageSource).toContain("from '@/components/shared/PageHeader'");
|
||||
expect(dashboardPageSource).toContain('<PageHeader');
|
||||
expect(dashboardPageSource).toContain('title="Dashboard"');
|
||||
expect(dashboardPageSource).toContain('<RelayOnboardingCard />');
|
||||
expect(dashboardPageSource).toContain(
|
||||
'ActionRequiredPanel,\n DashboardCustomizer,\n KPIStrip,\n ProblemResourcesTable,\n TrendCharts,',
|
||||
@@ -178,6 +181,7 @@ describe('Dashboard page module contract', () => {
|
||||
it('routes the empty dashboard state to infrastructure install', () => {
|
||||
render(() => <DashboardPage />);
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Dashboard' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'No resources yet' })).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('relay-onboarding-card')).toBeNull();
|
||||
expect(
|
||||
|
||||
@@ -15,6 +15,9 @@ describe('operations page route shell', () => {
|
||||
expect(operationsPageRouteSource).not.toContain('useNavigate');
|
||||
expect(operationsPageRouteSource).not.toContain('createSignal');
|
||||
expect(operationsPageSurfaceSource).toContain('@/components/shared/Subtabs');
|
||||
expect(operationsPageSurfaceSource).toContain("import { PageHeader } from '@/components/shared/PageHeader';");
|
||||
expect(operationsPageSurfaceSource).toContain('<PageHeader');
|
||||
expect(operationsPageSurfaceSource).toContain('title="Operations"');
|
||||
expect(operationsPageSurfaceSource).toContain('getOperationsTabFromPath');
|
||||
expect(operationsPageSurfaceSource).toContain('buildOperationsPath');
|
||||
expect(operationsPageSurfaceSource).toContain('operationsSurfaceHiddenInDemoMode');
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, render, screen, waitFor } from '@solidjs/testing-library';
|
||||
import { Route, Router } from '@solidjs/router';
|
||||
import PricingHandoff from '@/pages/PricingHandoff';
|
||||
import pricingHandoffSource from '@/pages/PricingHandoff.tsx?raw';
|
||||
import { getSelfHostedPurchaseStartUrl } from '@/utils/pricingHandoff';
|
||||
|
||||
const trackPaywallViewedMock = vi.fn();
|
||||
@@ -59,6 +60,12 @@ describe('PricingHandoff', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the pricing handoff on the shared page-header shell', () => {
|
||||
expect(pricingHandoffSource).toContain("import { PageHeader } from '@/components/shared/PageHeader';");
|
||||
expect(pricingHandoffSource).toContain('<PageHeader');
|
||||
expect(pricingHandoffSource).not.toContain('<h1');
|
||||
});
|
||||
|
||||
it('keeps monitored-system pricing handoffs inside the product', async () => {
|
||||
window.history.replaceState({}, '', '/pricing?feature=max_monitored_systems');
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ describe('storage page route shell', () => {
|
||||
);
|
||||
expect(storagePageRouteSource).toContain('<StorageSurface />');
|
||||
expect(storagePageRouteSource).not.toContain('useStoragePageModel');
|
||||
expect(storageSurfaceSource).toContain("import { PageHeader } from '@/components/shared/PageHeader';");
|
||||
expect(storageSurfaceSource).toContain('<PageHeader');
|
||||
expect(storageSurfaceSource).toContain('title="Storage"');
|
||||
expect(storageSurfaceSource).toContain('useStoragePageModel');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -211,6 +211,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertIn('--ref "$CURRENT_BRANCH"', helper)
|
||||
self.assertIn("Release automation executes the selected remote ref", helper)
|
||||
self.assertNotIn("Continue anyway?", helper)
|
||||
self.assertIn("Audit header composition", content)
|
||||
self.assertIn("run: npm --prefix frontend-modern run lint:headers", content)
|
||||
self.assertIn("pushed governed release-branch copy of `.github/workflows/release-dry-run.yml`", policy)
|
||||
self.assertIn("GitHub executes the selected remote ref", normalize_ws(policy))
|
||||
checklist = read("docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md")
|
||||
|
||||
Reference in New Issue
Block a user