diff --git a/apps/auth-ui/src/components/layout/Sidebar.tsx b/apps/auth-ui/src/components/layout/Sidebar.tsx
index b348ab4..dfe1483 100644
--- a/apps/auth-ui/src/components/layout/Sidebar.tsx
+++ b/apps/auth-ui/src/components/layout/Sidebar.tsx
@@ -1,6 +1,6 @@
import { Link, useLocation } from 'react-router-dom';
import { cn } from '../../lib/utils';
-import { Users, Link as LinkIcon, Globe, Menu, X, Key, Shield, FileCode } from 'lucide-react';
+import { Users, Link as LinkIcon, Globe, Menu, X, Key, Shield, FileCode, Package } from 'lucide-react';
import { Button } from '../ui/button';
import { SiteSwitcher } from './SiteSwitcher';
import { ThemeToggle } from '../theme-toggle';
@@ -35,6 +35,11 @@ export const Sidebar = ({ className, isCollapsed, onCollapse }: SidebarProps) =>
href: '/assets',
icon: FileCode,
},
+ {
+ title: 'Bundles',
+ href: '/bundles',
+ icon: Package,
+ },
{
title: 'JWT Inspector',
href: '/jwt-inspector',
diff --git a/apps/auth-ui/src/pages/bundles/BundleDetailsModal.tsx b/apps/auth-ui/src/pages/bundles/BundleDetailsModal.tsx
new file mode 100644
index 0000000..dd24702
--- /dev/null
+++ b/apps/auth-ui/src/pages/bundles/BundleDetailsModal.tsx
@@ -0,0 +1,63 @@
+import type { components } from 'auth-openapi';
+import { Badge } from '../../components/ui/badge';
+import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../components/ui/dialog';
+
+type Bundle = components['schemas']['bundle'];
+
+interface BundleDetailsModalProps {
+ bundle: Bundle;
+}
+
+export const BundleDetailsModal = ({ bundle }: BundleDetailsModalProps) => (
+ // Only the body scrolls, so a long list can't carry the close button off-screen with it.
+
+
+ Bundle {bundle.id}
+
+ {bundle.environment} ยท {bundle.revision}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Metadata
+
{JSON.stringify(bundle.metadata ?? {}, null, 2)}
+
+
+
+);
+
+const Field = ({ label, value }: { label: string; value?: string }) => (
+
+
{label}
+
{value ?? 'โ'}
+
+);
+
+const BundleEntryList = ({ title, emptyLabel, entries }: { title: string; emptyLabel: string; entries?: { name: string; version: number }[] }) => (
+
+
{title}
+ {entries && entries.length > 0 ? (
+ // Bounded so a long list doesn't bury what follows it.
+
+ {entries.map((entry, index) => (
+ // uniqueItems isn't enforced at this layer, so the index guards the key.
+ -
+ {entry.name}
+ v{entry.version}
+
+ ))}
+
+ ) : (
+
{emptyLabel}
+ )}
+
+);
diff --git a/apps/auth-ui/src/pages/bundles/BundlesPage.tsx b/apps/auth-ui/src/pages/bundles/BundlesPage.tsx
new file mode 100644
index 0000000..d542b45
--- /dev/null
+++ b/apps/auth-ui/src/pages/bundles/BundlesPage.tsx
@@ -0,0 +1,147 @@
+import type { components } from 'auth-openapi';
+import { Loader2 } from 'lucide-react';
+import { useState } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import { Button } from '../../components/ui/button';
+import { Dialog } from '../../components/ui/dialog';
+import { Input } from '../../components/ui/input';
+import { Label } from '../../components/ui/label';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
+import { $api } from '../../fetch';
+import { BundleDetailsModal } from './BundleDetailsModal';
+import { BundlesTable } from './BundlesTable';
+
+type Bundle = components['schemas']['bundle'];
+type Environment = components['schemas']['environment'];
+
+const ENVIRONMENTS: Environment[] = ['np', 'stage', 'prod'];
+
+/** Radix rejects an empty option value, so "no filter" needs a name of its own. */
+const ANY = 'all';
+
+const oneOf = (value: string | null, allowed: readonly T[], fallback: T): T =>
+ allowed.includes(value as T) ? (value as T) : fallback;
+
+// Reads the picked day as local time, not UTC, before converting to the wire format โ
+// same pattern as ClientsPage's Calendar picker.
+const dayBoundary = (date: string, end: boolean): string => {
+ const [year, month, day] = date.split('-').map(Number) as [number, number, number];
+ return end ? new Date(year, month - 1, day, 23, 59, 59, 999).toISOString() : new Date(year, month - 1, day, 0, 0, 0, 0).toISOString();
+};
+
+const startOfDay = (date: string): string => dayBoundary(date, false);
+const endOfDay = (date: string): string => dayBoundary(date, true);
+
+// No sort parameter on the endpoint, so newest-first is applied here.
+const byCreatedAtDesc = (a: Bundle, b: Bundle): number => new Date(b.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime();
+
+export const BundlesPage = () => {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const [selectedBundle, setSelectedBundle] = useState(null);
+
+ // Guards against a bad url value reaching the server as a 400.
+ const environment = oneOf(searchParams.get('environment'), ENVIRONMENTS, ANY);
+ const createdAfter = searchParams.get('createdAfter') ?? '';
+ const createdBefore = searchParams.get('createdBefore') ?? '';
+
+ const updateParams = (changes: Record) => {
+ setSearchParams(
+ (current) => {
+ const next = new URLSearchParams(current);
+ for (const [key, value] of Object.entries(changes)) {
+ if (value === null || value === '' || value === ANY) next.delete(key);
+ else next.set(key, value);
+ }
+ return next;
+ },
+ { replace: true }
+ );
+ };
+
+ const query = {
+ ...(environment === ANY ? {} : { environment: [environment] }),
+ ...(createdAfter && { createdAfter: startOfDay(createdAfter) }),
+ ...(createdBefore && { createdBefore: endOfDay(createdBefore) }),
+ };
+
+ const { data, isLoading, isError, error, refetch } = $api.useQuery('get', '/bundle', { params: { query } });
+
+ if (isError) {
+ return (
+
+
+
Failed to load bundles
+
{error?.message ?? 'Please try again later'}
+
+
+
+ );
+ }
+
+ return (
+
+
+
Bundles
+
+
+
+
+
+
+
+
+
+
+ updateParams({ createdAfter: event.target.value || null })}
+ />
+
+
+
+
+ updateParams({ createdBefore: event.target.value || null })}
+ />
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/apps/auth-ui/src/pages/bundles/BundlesTable.tsx b/apps/auth-ui/src/pages/bundles/BundlesTable.tsx
new file mode 100644
index 0000000..79b71ff
--- /dev/null
+++ b/apps/auth-ui/src/pages/bundles/BundlesTable.tsx
@@ -0,0 +1,50 @@
+import type { components } from 'auth-openapi';
+import { Badge } from '../../components/ui/badge';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../components/ui/table';
+import { formatTimestamp } from '../../lib/utils';
+
+type Bundle = components['schemas']['bundle'];
+
+interface BundlesTableProps {
+ bundles: Bundle[];
+ onSelectBundle: (bundle: Bundle) => void;
+}
+
+const COLUMNS = ['Created At', 'Environment', 'Revision', 'OPA Version', 'Id'];
+
+export const BundlesTable = ({ bundles, onSelectBundle }: BundlesTableProps) => (
+
+
+
+
+ {COLUMNS.map((label) => (
+ {label}
+ ))}
+
+
+
+ {bundles.length === 0 ? (
+
+
+ No bundles found.
+
+
+ ) : (
+ bundles.map((bundle) => (
+ onSelectBundle(bundle)} className="cursor-pointer hover:bg-muted/50">
+ {bundle.createdAt ? formatTimestamp(bundle.createdAt) : 'โ'}
+
+
+ {bundle.environment}
+
+
+ {bundle.revision}
+ {bundle.opaVersion}
+ {bundle.id}
+
+ ))
+ )}
+
+
+
+);
diff --git a/apps/auth-ui/src/pages/bundles/index.ts b/apps/auth-ui/src/pages/bundles/index.ts
new file mode 100644
index 0000000..c69f00e
--- /dev/null
+++ b/apps/auth-ui/src/pages/bundles/index.ts
@@ -0,0 +1 @@
+export { BundlesPage } from './BundlesPage';
diff --git a/apps/auth-ui/src/routes.tsx b/apps/auth-ui/src/routes.tsx
index 25d5c9b..6c85191 100644
--- a/apps/auth-ui/src/routes.tsx
+++ b/apps/auth-ui/src/routes.tsx
@@ -1,6 +1,7 @@
import { Navigate, type RouteObject } from 'react-router-dom';
import { Layout } from './components/layout';
import { AssetPage, AssetsPage, CreateAssetPage } from './pages/assets';
+import { BundlesPage } from './pages/bundles';
import { ClientsPage } from './pages/clients';
import { ConnectionsPage } from './pages/connections';
import { DomainsPage } from './pages/domains';
@@ -32,6 +33,7 @@ export const appRoutes: RouteObject[] = [
{ path: ':assetName', element: },
],
},
+ { path: 'bundles', element: },
{ path: 'jwt-inspector', element: },
{ path: 'opa-validator', element: },
{ path: '*', element: },
diff --git a/apps/auth-ui/tests/assets/assets-list.spec.tsx b/apps/auth-ui/tests/assets/assets-list.spec.tsx
index a5496ec..fe9753e 100644
--- a/apps/auth-ui/tests/assets/assets-list.spec.tsx
+++ b/apps/auth-ui/tests/assets/assets-list.spec.tsx
@@ -28,7 +28,7 @@ describe('assets list', () => {
const entries = within(nav)
.getAllByRole('link')
.map((link) => link.textContent);
- expect(entries).toEqual(['Clients', 'Connections', 'Domains', 'Assets', 'JWT Inspector', 'OPA Validator']);
+ expect(entries).toEqual(['Clients', 'Connections', 'Domains', 'Assets', 'Bundles', 'JWT Inspector', 'OPA Validator']);
});
it('marks the Assets entry in the sidebar from anywhere underneath it', async () => {
diff --git a/apps/auth-ui/tests/bundle-fixtures.ts b/apps/auth-ui/tests/bundle-fixtures.ts
new file mode 100644
index 0000000..1e4ecf4
--- /dev/null
+++ b/apps/auth-ui/tests/bundle-fixtures.ts
@@ -0,0 +1,17 @@
+import type { components } from 'auth-openapi';
+
+export type Bundle = components['schemas']['bundle'];
+
+export const aBundle = (overrides: Partial = {}): Bundle => ({
+ id: 1,
+ hash: 'sha256:abc123',
+ revision: 'rev-1',
+ metadata: { builtBy: 'ci' },
+ assets: [{ name: 'authz.rego', version: 1 }],
+ connections: [{ name: 'my-connection', version: 1 }],
+ environment: 'np',
+ createdAt: '2026-01-01T00:00:00.000Z',
+ keyVersion: 1,
+ opaVersion: '0.60.0',
+ ...overrides,
+});
diff --git a/apps/auth-ui/tests/bundles/bundle-details-modal.spec.tsx b/apps/auth-ui/tests/bundles/bundle-details-modal.spec.tsx
new file mode 100644
index 0000000..621505b
--- /dev/null
+++ b/apps/auth-ui/tests/bundles/bundle-details-modal.spec.tsx
@@ -0,0 +1,105 @@
+import { screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { describe, expect, it, vi } from 'vitest';
+import { appRoutes } from '../../src/routes';
+import { aBundle } from '../bundle-fixtures';
+import { http } from '../http-stub';
+import { renderRoutes } from '../render';
+
+const openBundles = (search = '') => renderRoutes(appRoutes, `/bundles${search}`);
+
+describe('bundle details modal', () => {
+ it('opens from a row and shows the hash, keyVersion, assets, connections and metadata', async () => {
+ http.on('GET', '/bundle', {
+ body: [
+ aBundle({
+ id: 7,
+ hash: 'sha256:deadbeef',
+ keyVersion: 3,
+ assets: [{ name: 'authz.rego', version: 2 }],
+ connections: [{ name: 'billing-connection', version: 5 }],
+ metadata: { builtBy: 'ci', commit: 'abc123' },
+ }),
+ ],
+ });
+
+ openBundles();
+ await userEvent.click(await screen.findByText('rev-1'));
+
+ const dialog = await screen.findByRole('dialog');
+ expect(within(dialog).getByText('sha256:deadbeef')).toBeInTheDocument();
+ expect(within(dialog).getByText('3')).toBeInTheDocument();
+ expect(within(dialog).getByText('authz.rego')).toBeInTheDocument();
+ expect(within(dialog).getByText('v2')).toBeInTheDocument();
+ expect(within(dialog).getByText('billing-connection')).toBeInTheDocument();
+ expect(within(dialog).getByText('v5')).toBeInTheDocument();
+ expect(within(dialog).getByText(/"builtBy": "ci"/)).toBeInTheDocument();
+ expect(within(dialog).getByText(/"commit": "abc123"/)).toBeInTheDocument();
+ });
+
+ it('shows a placeholder when a bundle has no assets or connections', async () => {
+ http.on('GET', '/bundle', { body: [aBundle({ assets: [], connections: [] })] });
+
+ openBundles();
+ await userEvent.click(await screen.findByText('rev-1'));
+
+ const dialog = await screen.findByRole('dialog');
+ expect(within(dialog).getByText('No assets')).toBeInTheDocument();
+ expect(within(dialog).getByText('No connections')).toBeInTheDocument();
+ });
+
+ it('makes no additional request when opening the modal', async () => {
+ http.on('GET', '/bundle', { body: [aBundle()] });
+
+ openBundles();
+ await userEvent.click(await screen.findByText('rev-1'));
+
+ await screen.findByRole('dialog');
+ expect(http.requestsFor('GET', '/bundle')).toHaveLength(1);
+ });
+
+ it('is read-only, with no edit or delete controls', async () => {
+ http.on('GET', '/bundle', { body: [aBundle()] });
+
+ openBundles();
+ await userEvent.click(await screen.findByText('rev-1'));
+
+ const dialog = await screen.findByRole('dialog');
+ expect(within(dialog).queryByRole('button', { name: /edit/i })).not.toBeInTheDocument();
+ expect(within(dialog).queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
+ });
+
+ it('renders a large connections list, keeping the close button reachable and each row uniquely keyed', async () => {
+ // Duplicate name+version pair, despite uniqueItems in the schema.
+ const manyConnections = Array.from({ length: 320 }, (_, index) => ({ name: `connection-${index % 300}`, version: 1 }));
+ http.on('GET', '/bundle', { body: [aBundle({ connections: manyConnections })] });
+
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
+
+ openBundles();
+ await userEvent.click(await screen.findByText('rev-1'));
+
+ const dialog = await screen.findByRole('dialog');
+ expect(within(dialog).getAllByText('connection-0')).toHaveLength(2);
+ expect(within(dialog).getByText('connection-299')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
+
+ const keyWarning = consoleError.mock.calls.find((call) => typeof call[0] === 'string' && call[0].includes('two children with the same key'));
+ expect(keyWarning).toBeUndefined();
+
+ consoleError.mockRestore();
+ });
+
+ it('closes the modal without affecting the underlying list', async () => {
+ http.on('GET', '/bundle', { body: [aBundle()] });
+
+ openBundles();
+ await userEvent.click(await screen.findByText('rev-1'));
+ await screen.findByRole('dialog');
+
+ await userEvent.click(screen.getByRole('button', { name: /close/i }));
+
+ await screen.findByText('rev-1');
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
+});
diff --git a/apps/auth-ui/tests/bundles/bundles-list.spec.tsx b/apps/auth-ui/tests/bundles/bundles-list.spec.tsx
new file mode 100644
index 0000000..d672fde
--- /dev/null
+++ b/apps/auth-ui/tests/bundles/bundles-list.spec.tsx
@@ -0,0 +1,168 @@
+import { screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { describe, expect, it } from 'vitest';
+import { appRoutes } from '../../src/routes';
+import { aBundle } from '../bundle-fixtures';
+import { http } from '../http-stub';
+import { renderRoutes } from '../render';
+
+const openBundles = (search = '') => renderRoutes(appRoutes, `/bundles${search}`);
+
+/** Round-trips through local getters instead of a hardcoded UTC literal, since the offset depends on the host's timezone. */
+const expectLocalDayBoundary = (iso: string | null | undefined, year: number, month: number, day: number, end: boolean): void => {
+ expect(iso).not.toBeNull();
+ const parsed = new Date(iso!);
+ expect([
+ parsed.getFullYear(),
+ parsed.getMonth() + 1,
+ parsed.getDate(),
+ parsed.getHours(),
+ parsed.getMinutes(),
+ parsed.getSeconds(),
+ parsed.getMilliseconds(),
+ ]).toEqual([year, month, day, ...(end ? [23, 59, 59, 999] : [0, 0, 0, 0])]);
+};
+
+describe('bundles list', () => {
+ it('appends a Bundles entry to the sidebar after Assets, leaving the others in place', async () => {
+ http.on('GET', '/bundle', { body: [] });
+
+ openBundles();
+
+ const nav = await screen.findByRole('navigation');
+ const entries = within(nav)
+ .getAllByRole('link')
+ .map((link) => link.textContent);
+ expect(entries).toEqual(['Clients', 'Connections', 'Domains', 'Assets', 'Bundles', 'JWT Inspector', 'OPA Validator']);
+ });
+
+ it('renders a table fetched from GET /bundle with no query parameters', async () => {
+ http.on('GET', '/bundle', { body: [aBundle()] });
+
+ openBundles();
+
+ expect(await screen.findByText('rev-1')).toBeInTheDocument();
+ expect(http.lastRequestFor('GET', '/bundle')?.query.toString()).toBe('');
+ });
+
+ it('shows Created At, Environment, Revision, OPA Version, and Id columns in that order', async () => {
+ http.on('GET', '/bundle', { body: [aBundle({ id: 42, revision: 'rev-7', opaVersion: '0.61.0', environment: 'prod' })] });
+
+ openBundles();
+
+ const table = await screen.findByRole('table');
+ const headers = within(table)
+ .getAllByRole('columnheader')
+ .map((header) => header.textContent);
+ expect(headers).toEqual(['Created At', 'Environment', 'Revision', 'OPA Version', 'Id']);
+
+ const row = within(table).getAllByRole('row')[1]!;
+ expect(within(row).getByText('prod')).toBeInTheDocument();
+ expect(within(row).getByText('rev-7')).toBeInTheDocument();
+ expect(within(row).getByText('0.61.0')).toBeInTheDocument();
+ expect(within(row).getByText('42')).toBeInTheDocument();
+ });
+
+ it('orders bundles with the newest createdAt first, regardless of the order returned by the server', async () => {
+ http.on('GET', '/bundle', {
+ body: [
+ aBundle({ id: 1, createdAt: '2026-01-01T00:00:00.000Z' }),
+ aBundle({ id: 3, createdAt: '2026-03-01T00:00:00.000Z' }),
+ aBundle({ id: 2, createdAt: '2026-02-01T00:00:00.000Z' }),
+ ],
+ });
+
+ openBundles();
+
+ const table = await screen.findByRole('table');
+ const idsInOrder = within(table)
+ .getAllByRole('row')
+ .slice(1)
+ .map((row) => within(row).getAllByRole('cell').at(-1)?.textContent);
+ expect(idsInOrder).toEqual(['3', '2', '1']);
+ });
+
+ it('shows a loading state while the request is in flight', async () => {
+ http.on('GET', '/bundle', { body: [aBundle()], delayMs: 30 });
+
+ openBundles();
+
+ expect(await screen.findByRole('status', { name: 'Loading bundles' })).toBeInTheDocument();
+ });
+
+ it('shows an empty state when no bundles are returned', async () => {
+ http.on('GET', '/bundle', { body: [] });
+
+ openBundles();
+
+ expect(await screen.findByText('No bundles found.')).toBeInTheDocument();
+ });
+
+ it('renders no create, edit, or delete controls', async () => {
+ http.on('GET', '/bundle', { body: [aBundle()] });
+
+ openBundles();
+ await screen.findByText('rev-1');
+
+ expect(screen.queryByRole('button', { name: /add bundle/i })).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /edit/i })).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
+ });
+
+ it('sends no environment filter until one is chosen, then sends it as a one-element array', async () => {
+ http.on('GET', '/bundle', { body: [aBundle()] });
+
+ openBundles();
+ await screen.findByText('rev-1');
+ expect(http.lastRequestFor('GET', '/bundle')?.query.getAll('environment')).toEqual([]);
+
+ await userEvent.click(screen.getByRole('combobox', { name: 'Environment' }));
+ await userEvent.click(await screen.findByRole('option', { name: 'prod' }));
+
+ await waitFor(() => expect(http.lastRequestFor('GET', '/bundle')?.query.getAll('environment')).toEqual(['prod']));
+ });
+
+ it('sends the createdAfter and createdBefore date filters to the server as the local midnight/end-of-day of the picked day', async () => {
+ http.on('GET', '/bundle', { body: [aBundle()] });
+
+ openBundles();
+ await screen.findByText('rev-1');
+
+ await userEvent.type(screen.getByLabelText('Created after'), '2026-01-01');
+ await waitFor(() => expectLocalDayBoundary(http.lastRequestFor('GET', '/bundle')?.query.get('createdAfter'), 2026, 1, 1, false));
+
+ await userEvent.type(screen.getByLabelText('Created before'), '2026-06-01');
+ await waitFor(() => expectLocalDayBoundary(http.lastRequestFor('GET', '/bundle')?.query.get('createdBefore'), 2026, 6, 1, true));
+ });
+
+ it('combines the environment and date filters into a single request', async () => {
+ http.on('GET', '/bundle', { body: [aBundle()] });
+
+ openBundles('?environment=stage&createdAfter=2026-01-01&createdBefore=2026-06-01');
+ await screen.findByText('rev-1');
+
+ const request = http.lastRequestFor('GET', '/bundle');
+ expect(request?.query.getAll('environment')).toEqual(['stage']);
+ expectLocalDayBoundary(request?.query.get('createdAfter'), 2026, 1, 1, false);
+ expectLocalDayBoundary(request?.query.get('createdBefore'), 2026, 6, 1, true);
+ });
+
+ it('reads the filters back out of the url on load, and restores them across a refresh', async () => {
+ http.on('GET', '/bundle', { body: [aBundle()] });
+
+ openBundles('?environment=prod&createdAfter=2026-01-01&createdBefore=2026-06-01');
+
+ await screen.findByText('rev-1');
+ expect(screen.getByRole('combobox', { name: 'Environment' })).toHaveTextContent('prod');
+ expect(screen.getByLabelText('Created after')).toHaveValue('2026-01-01');
+ expect(screen.getByLabelText('Created before')).toHaveValue('2026-06-01');
+ });
+
+ it('shows an empty state when the active filters match no bundles', async () => {
+ http.on('GET', '/bundle', { body: [] });
+
+ openBundles('?environment=prod');
+
+ expect(await screen.findByText('No bundles found.')).toBeInTheDocument();
+ });
+});
diff --git a/apps/auth-ui/vitest.config.ts b/apps/auth-ui/vitest.config.ts
index 5bc3ffd..28dfd20 100644
--- a/apps/auth-ui/vitest.config.ts
+++ b/apps/auth-ui/vitest.config.ts
@@ -21,7 +21,7 @@ export default defineConfig({
environment: 'jsdom',
environmentOptions: { jsdom: { url: 'http://localhost:3000/' } },
setupFiles: ['./tests/setup.ts'],
- // Scoped to the assets feature. The rest of the application has no tests yet.
- include: ['tests/assets/**/*.spec.{ts,tsx}'],
+ // Scoped to the assets and bundles features. The rest of the application has no tests yet.
+ include: ['tests/assets/**/*.spec.{ts,tsx}', 'tests/bundles/**/*.spec.{ts,tsx}'],
},
});