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
47 changes: 41 additions & 6 deletions apps/frontend/src/app/pages/DependenciesPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,41 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useDependencies } from '@/features/dependencies/hooks/useDependencies';
import type { DependencyDiagnostic, DependencyGraphResponse } from '@/shared/services/api/types';
import { architectureService } from '@/shared/services/api/architecture';
import type { DependencyDiagnostic, DependencyGraphResponse, RevisionManifestResponse } from '@/shared/services/api/types';
import { DependenciesPage } from './DependenciesPage';

vi.mock('@/features/dependencies/hooks/useDependencies', () => ({
useDependencies: vi.fn(),
}));

vi.mock('@/shared/services/api/architecture', () => ({
architectureService: { getRevisionManifest: vi.fn() },
}));

const revisionManifest: RevisionManifestResponse = {
manifest: {
schemaVersion: 'revision-manifest.v1',
repositoryId: 'repo-1',
revisionKind: 'upload',
revisionValue: `sha256:${'0'.repeat(64)}`,
revisionRef: null,
snapshotId: 'snap_example',
snapshotSchemaVersion: 'ri.v1',
extractors: [{ name: 'dependency-manifest', version: '1.2.0' }],
producerSetHash: `sha256:${'1'.repeat(64)}`,
configHash: `sha256:${'2'.repeat(64)}`,
canonicalGraphHash: `sha256:${'3'.repeat(64)}`,
createdAt: '2026-07-25T00:00:00Z',
sealedAt: '2026-07-25T00:00:05Z',
},
manifestDigest: `sha256:${'4'.repeat(64)}`,
verificationMethod: 'sha256-canonical-json',
verificationState: 'verified',
verificationNote: 'This digest is a SHA-256 over the canonical JSON encoding of the manifest fields above.',
};

const graph: DependencyGraphResponse = {
schemaVersion: 'dependency-graph.v2',
repositoryId: 'repo-1',
Expand Down Expand Up @@ -124,6 +151,7 @@ function diagnostic(severity: DependencyDiagnostic['severity']): DependencyDiagn
describe('DependenciesPage', () => {
beforeEach(() => {
mockDependencies();
vi.mocked(architectureService.getRevisionManifest).mockResolvedValue(revisionManifest);
});

it('shows uncomputed assessments without clean badges or numeric fallbacks', () => {
Expand All @@ -136,14 +164,21 @@ describe('DependenciesPage', () => {
expect(screen.queryByText('outdated')).not.toBeInTheDocument();
});

it('renders the sealed snapshot identity, consistent with Architecture/Review', () => {
it('keeps the sealed snapshot identity one click away instead of on the landing strip (#176)', async () => {
renderPage();

expect(screen.getByText('Snapshot')).toBeInTheDocument();
await waitFor(() => expect(screen.getByTestId('revision-manifest')).toBeInTheDocument());
// Collapsed by default: the snapshot id stays visible, but the hashes do not.
expect(screen.getByText('snap_example')).toBeInTheDocument();
expect(screen.getByText('Canonical graph hash')).toBeInTheDocument();
expect(screen.getByText(graph.canonicalGraphHash)).toBeInTheDocument();
expect(screen.queryByText(revisionManifest.manifestDigest)).not.toBeInTheDocument();
expect(screen.queryByText(revisionManifest.manifest.canonicalGraphHash!)).not.toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: /details/i }));

expect(screen.getByText('Manifest digest')).toBeInTheDocument();
expect(screen.getByText(revisionManifest.manifestDigest)).toBeInTheDocument();
expect(screen.getByText('Canonical graph hash')).toBeInTheDocument();
expect(screen.getByText(revisionManifest.manifest.canonicalGraphHash!)).toBeInTheDocument();
});

it('shows an empty inventory instead of a search-miss message', () => {
Expand Down
19 changes: 4 additions & 15 deletions apps/frontend/src/app/pages/DependenciesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { EmptyState } from '@/shared/components/ui/EmptyState';
import { DataSourceBadge } from '@/shared/components/ui/DataSourceBadge';
import { ExportMenu } from '@/shared/components/ui/ExportMenu';
import { useDependencies } from '@/features/dependencies/hooks/useDependencies';
import { RevisionManifestPanel } from '@/features/architecture/components/RevisionManifestPanel';
import type { DependencyAssessment } from '@/shared/services/api/types';

export function DependenciesPage() {
Expand Down Expand Up @@ -121,12 +122,9 @@ export function DependenciesPage() {
<DataSourceBadge source={dependencies.source} />
</PageHeader>

<section aria-label="Snapshot identity" className="mb-6 grid gap-3 rounded-xl border border-border bg-card p-4 sm:grid-cols-2 lg:grid-cols-4">
<Identity label="Snapshot" value={graph.snapshotId} />
<Identity label="Snapshot schema" value={graph.snapshotSchemaVersion} />
<Identity label="Manifest digest" value={graph.manifestDigest} />
<Identity label="Canonical graph hash" value={graph.canonicalGraphHash} />
</section>
<div className="mb-6">
<RevisionManifestPanel repositoryId={activeRepository.id} />
</div>

<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mb-6">
<Stat label="Dependencies" value={graph.totalDependencies} />
Expand Down Expand Up @@ -227,12 +225,3 @@ function Stat({ label, value }: { label: string; value: number | string }) {
</div>
);
}

function Identity({ label, value }: { label: string; value: string }) {
return (
<div className="min-w-0">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">{label}</p>
<p className="truncate font-mono text-xs text-foreground" title={value}>{value}</p>
</div>
);
}
134 changes: 134 additions & 0 deletions apps/frontend/src/app/pages/EngineeringReviewPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useReview } from '@/features/review/hooks/useReview';
import { useReviewStore } from '@/features/review/store';
import { architectureService } from '@/shared/services/api/architecture';
import type { EngineeringReview } from '@/shared/types/review';
import type { RevisionManifestResponse } from '@/shared/services/api/types';
import { EngineeringReviewPage } from './EngineeringReviewPage';

vi.mock('@/features/review/hooks/useReview', () => ({
useReview: vi.fn(),
}));

vi.mock('@/shared/services/api/architecture', () => ({
architectureService: { getRevisionManifest: vi.fn() },
}));

const review: EngineeringReview = {
schemaVersion: 'engineering-review.v2',
repositoryId: 'repo-1',
repositoryName: 'sample',
revisionKind: 'upload',
revisionValue: `sha256:${'0'.repeat(64)}`,
snapshotId: 'snap_example',
snapshotSchemaVersion: 'ri.v1',
canonicalGraphHash: `sha256:${'1'.repeat(64)}`,
manifestDigest: `sha256:${'2'.repeat(64)}`,
provenance: {
source: 'ri.v1',
snapshotId: 'snap_example',
snapshotSchemaVersion: 'ri.v1',
canonicalGraphHash: `sha256:${'1'.repeat(64)}`,
},
generatedAt: '2026-07-25T00:00:00Z',
assessmentStatus: 'assessed',
categories: [],
findings: [],
summary: {
message: 'No findings were surfaced for this snapshot.',
findingsBySeverity: { info: 0, low: 0, medium: 0, high: 0, critical: 0 },
assessedCategories: 8,
partiallyAssessedCategories: 0,
notAssessedCategories: 0,
insufficientEvidenceCategories: 0,
evidenceBackedFindingCount: 0,
fileScopedFindingCount: 0,
omittedUnsupportedDiagnosticCount: 0,
vulnerabilityScanning: 'not_assessed',
},
};

const revisionManifest: RevisionManifestResponse = {
manifest: {
schemaVersion: 'revision-manifest.v1',
repositoryId: 'repo-1',
revisionKind: 'upload',
revisionValue: `sha256:${'0'.repeat(64)}`,
revisionRef: null,
snapshotId: 'snap_example',
snapshotSchemaVersion: 'ri.v1',
extractors: [{ name: 'typescript-extractor', version: '1.0.0' }],
producerSetHash: `sha256:${'1'.repeat(64)}`,
configHash: `sha256:${'2'.repeat(64)}`,
canonicalGraphHash: `sha256:${'3'.repeat(64)}`,
createdAt: '2026-07-25T00:00:00Z',
sealedAt: '2026-07-25T00:00:05Z',
},
manifestDigest: `sha256:${'4'.repeat(64)}`,
verificationMethod: 'sha256-canonical-json',
verificationState: 'verified',
verificationNote: 'This digest is a SHA-256 over the canonical JSON encoding of the manifest fields above.',
};

function renderPage() {
return render(
<MemoryRouter>
<EngineeringReviewPage />
</MemoryRouter>,
);
}

describe('EngineeringReviewPage', () => {
beforeEach(() => {
vi.mocked(useReview).mockReturnValue({
review,
data: review,
source: 'upload',
status: 'success',
loading: false,
error: null,
empty: false,
success: true,
retry: vi.fn(),
refresh: vi.fn(),
activeRepository: null,
completedRepositories: [],
emptyReason: null,
});
useReviewStore.setState({
review,
selectedFindingId: null,
filterCategory: 'all',
filterSeverity: 'all',
filterDiagnosticCode: null,
});
vi.mocked(architectureService.getRevisionManifest).mockResolvedValue(revisionManifest);
});

it('keeps the sealed snapshot identity one click away instead of on the landing strip (#176)', async () => {
renderPage();

await waitFor(() => expect(screen.getByTestId('revision-manifest')).toBeInTheDocument());
expect(screen.getByText('snap_example')).toBeInTheDocument();
expect(screen.queryByText(revisionManifest.manifestDigest)).not.toBeInTheDocument();
expect(screen.queryByText(revisionManifest.manifest.canonicalGraphHash!)).not.toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: /details/i }));

expect(screen.getByText('Manifest digest')).toBeInTheDocument();
expect(screen.getByText(revisionManifest.manifestDigest)).toBeInTheDocument();
expect(screen.getByText(revisionManifest.manifest.canonicalGraphHash!)).toBeInTheDocument();
});

it('still renders the evidence-backed summary and assessment matrix', () => {
renderPage();

expect(screen.getByText('Evidence-backed summary')).toBeInTheDocument();
expect(screen.getByText('No findings were surfaced for this snapshot.')).toBeInTheDocument();
expect(
screen.getByText('No overall score, grade, health percentage, or category score is produced.'),
).toBeInTheDocument();
});
});
19 changes: 4 additions & 15 deletions apps/frontend/src/app/pages/EngineeringReviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FindingCard } from '@/features/review/components/FindingCard';
import { FindingDetail } from '@/features/review/components/FindingDetail';
import { ReviewFilters } from '@/features/review/components/ReviewFilters';
import { ExportMenu } from '@/shared/components/ui/ExportMenu';
import { RevisionManifestPanel } from '@/features/architecture/components/RevisionManifestPanel';
import type { ReviewAssessmentState, ReviewSeverity } from '@/shared/types/review';

const STATE_LABEL: Record<ReviewAssessmentState, string> = {
Expand Down Expand Up @@ -102,12 +103,9 @@ export function EngineeringReviewPage() {
categories without sufficient evidence are marked Not assessed.
</div>

<section aria-label="Review identity" className="mb-6 grid gap-3 rounded-xl border border-border bg-card p-4 sm:grid-cols-2 lg:grid-cols-4">
<Identity label="Snapshot" value={review.snapshotId} />
<Identity label="Snapshot schema" value={review.snapshotSchemaVersion} />
<Identity label="Manifest digest" value={review.manifestDigest} />
<Identity label="Canonical graph hash" value={review.canonicalGraphHash} />
</section>
<div className="mb-6">
<RevisionManifestPanel repositoryId={review.repositoryId} />
</div>

<section className="mb-6 rounded-xl border border-border bg-card p-5">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
Expand Down Expand Up @@ -207,15 +205,6 @@ export function EngineeringReviewPage() {
);
}

function Identity({ label, value }: { label: string; value: string }) {
return (
<div className="min-w-0">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">{label}</p>
<p className="truncate font-mono text-xs text-foreground" title={value}>{value}</p>
</div>
);
}

function StatusPanel({ text }: { text: string }) {
return (
<div className="flex min-h-72 items-center justify-center gap-3">
Expand Down
77 changes: 77 additions & 0 deletions apps/frontend/src/app/pages/RepositoryDetailPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { describe, expect, it, vi } from 'vitest';
import type { Repository } from '@/shared/types';
import { RepositoryDetailPage } from './RepositoryDetailPage';

const repositoryState = vi.hoisted(() => ({ repositories: [] as Repository[] }));

vi.mock('@/features/repositories/hooks/useRepository', () => ({
useRepository: () => ({ repositories: repositoryState.repositories }),
}));

vi.mock('@/features/repositories/components/RepositoryOutcomeSummary', () => ({
RepositoryOutcomeSummary: ({ repository }: { repository: Repository }) => (
<div data-testid="outcome-summary-stub">Outcome summary for {repository.name}</div>
),
}));

const completedRepository: Repository = {
id: 'repo-1',
name: 'sample',
source: 'upload',
size: 10,
fileCount: 5,
status: 'completed',
analysisStage: 'completed',
analysisProgress: 100,
uploadedAt: '2026-07-22T08:00:00Z',
meta: {
language: 'TypeScript',
framework: 'React',
totalFiles: 5,
totalFolders: 2,
entryPoint: 'src/main.tsx',
configFiles: [],
packageManager: 'npm',
hasReadme: true,
hasLicense: true,
licenseName: 'MIT',
},
fileTree: [],
};

function renderPage(repositoryId = 'repo-1') {
return render(
<MemoryRouter initialEntries={[`/repositories/${repositoryId}`]}>
<Routes>
<Route path="/repositories/:id" element={<RepositoryDetailPage />} />
</Routes>
</MemoryRouter>,
);
}

describe('RepositoryDetailPage', () => {
it('leads the completed repository landing view with the outcome summary, before the tabs (#176)', () => {
repositoryState.repositories = [completedRepository];

renderPage();

const summary = screen.getByTestId('outcome-summary-stub');
expect(summary).toHaveTextContent('Outcome summary for sample');

const overviewTab = screen.getByRole('button', { name: 'Overview' });
expect(summary.compareDocumentPosition(overviewTab) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});

it('does not show the outcome summary for a repository that failed analysis', () => {
repositoryState.repositories = [
{ ...completedRepository, status: 'error', errorMessage: 'Extraction crashed.', meta: null },
];

renderPage();

expect(screen.queryByTestId('outcome-summary-stub')).not.toBeInTheDocument();
expect(screen.getByText('Extraction crashed.')).toBeInTheDocument();
});
});
3 changes: 3 additions & 0 deletions apps/frontend/src/app/pages/RepositoryDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { EmptyState } from '@/shared/components/ui/EmptyState';
import { Badge } from '@/shared/components/ui/Badge';
import { DataSourceBadge } from '@/shared/components/ui/DataSourceBadge';
import { RepositoryExplorer } from '@/features/explorer/components/RepositoryExplorer';
import { RepositoryOutcomeSummary } from '@/features/repositories/components/RepositoryOutcomeSummary';
import { useRepositoryDetail } from '@/features/repositories/hooks/useRepositoryDetail';
import { useRepositoryTree } from '@/features/repositories/hooks/useRepositoryTree';
import { repositoryStatusVariant } from '@/features/repositories/status';
Expand Down Expand Up @@ -85,6 +86,8 @@ export function RepositoryDetailPage() {
</div>
) : repo.status === 'completed' && repo.meta ? (
<>
<RepositoryOutcomeSummary repository={repo} />

<div className="flex items-center gap-1 border-b border-border mb-6">
{tabs.map((tab) => (
<button
Expand Down
Loading
Loading