Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
631c218
docs: add dashboard plugin and graph-parity test plan
nicolejms Sep 11, 2026
1931b03
docs: add graph test taxonomy and reconcile test plan with the plugin…
nicolejms Sep 11, 2026
f73d978
docs: state the test tooling and the runner boundary it creates
nicolejms Sep 11, 2026
c8f3b63
docs: record why Jest is kept and when to revisit Vitest
nicolejms Sep 11, 2026
24d49f2
docs: record which repository each phase depends on
nicolejms Sep 11, 2026
8b498a5
Execute test plan phases 0 and 3, and begin phase 1
nicolejms Sep 11, 2026
308c352
Begin test plan phase 2: graph fixtures and Tier A invariants
nicolejms Sep 11, 2026
fd90d93
Continue test plan phase 1: cover the two pages at zero coverage
nicolejms Sep 11, 2026
6235f78
Add phase 9: migrate the test corpus from Applications.Core to Radius.*
nicolejms Sep 11, 2026
cfee32c
Link tracked defects to upstream issues
nicolejms Sep 11, 2026
71863e7
test(plugin-radius): extend Phase 1 coverage to component suites
nicolejms Sep 11, 2026
844d60d
test: close regression gaps identified in PR review
nicolejms Sep 11, 2026
84102a9
test: complete Phase 1 by covering the last untested shipped files
nicolejms Sep 11, 2026
78ba43d
docs: record the Sucrase transform cache-key trap as a tracked guardrail
nicolejms Sep 11, 2026
e5c637f
test: close Phase 1 review gaps and harden coverage policy
nicolejms Sep 11, 2026
7daa3bb
test: freeze the Phase 2 graph baseline
nicolejms Sep 13, 2026
e5459bb
docs: correct Phase 2 defect references
nicolejms Sep 13, 2026
f754657
test: qualify the Phase 3 plugin package
nicolejms Sep 14, 2026
8aa9a11
fix: restore deferred plugin release decisions
nicolejms Sep 14, 2026
eb653ff
test: preserve plugin source manifest after packing
nicolejms Sep 14, 2026
ee66e90
test: guard both package manifests during packing
nicolejms Sep 14, 2026
b268103
Merge branch 'main' into nicolejms-phase-1-completion
nicolejms Sep 15, 2026
e251ef0
test: harden graph regression safeguards
nicolejms Sep 15, 2026
3764334
test: address approver-review findings on the Phase 0-3 baseline
nicolejms Sep 15, 2026
314f230
test: keep spawning Storybook when the app webserver is handed over
nicolejms Sep 15, 2026
1fd0392
test: detect graph defects by invariant and gate baseline edits
nicolejms Sep 15, 2026
ba373b6
ci: restore the Lint step dropped while adding fetch-depth
nicolejms Sep 15, 2026
a6599ad
fix(graph): build the Dagre graph per layout and drop coordinates fro…
nicolejms Sep 16, 2026
05c9e1d
test: take packaging qualification out of the unit run and trim revie…
nicolejms Sep 16, 2026
559c094
docs: record the namespace and repo-policy review decisions
nicolejms Sep 16, 2026
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
15 changes: 15 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# GU-25 compares the committed graph records against the base branch
# to catch baseline edits that are not declared in the expected-change
# manifest, so the base commit has to be present in the clone.
fetch-depth: 0

- name: Parse release version and set environment variables
run: python ./.github/scripts/get_release_version.py
Expand Down Expand Up @@ -67,12 +71,23 @@ jobs:

- name: Run Tests
if: ${{ env.CI_TEST == 'true' }}
env:
# Empty on push builds, where GU-25 falls back to origin/main.
GRAPH_RECORD_BASE_REF: ${{ github.event.pull_request.base.sha }}
run: yarn run test:all

- name: Run E2E Tests
if: ${{ env.CI_TEST == 'true' }}
run: yarn run test:e2e

- name: Run Component Browser Tests
if: ${{ env.CI_TEST == 'true' }}
run: yarn run test:e2e:components

- name: Qualify Package Artifacts
if: ${{ env.CI_TEST == 'true' }}
run: yarn run test:package

build-and-publish-container:
name: Build and Publish Container
runs-on: ubuntu-24.04
Expand Down
1,725 changes: 1,725 additions & 0 deletions docs/design/2026-09-dashboard-plugin-test-plan.md

Large diffs are not rendered by default.

527 changes: 527 additions & 0 deletions docs/design/2026-09-radius-backstage-plugin.md

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"test": "backstage-cli repo test",
"test:all": "backstage-cli repo test --coverage",
"test:e2e": "playwright test",
"test:e2e:components": "playwright test --config playwright.components.config.ts",
"test:package": "node scripts/test-package.js",
"fix": "backstage-cli repo fix",
"lint": "backstage-cli repo lint --since origin/main",
"lint:all": "backstage-cli repo lint",
Expand Down Expand Up @@ -67,5 +69,42 @@
"*.{json,md}": [
"prettier --write"
]
},
"jest": {
"coveragePathIgnorePatterns": [
"/__docs__/"
],
"coverageThreshold": {
"./plugins/plugin-radius/src/": {
"statements": 73,
"branches": 55,
"functions": 68,
"lines": 74
},
"./plugins/plugin-radius-backend/src/": {
"statements": 93,
"branches": 100,
"functions": 100,
"lines": 100
},
"./packages/rad-components/src/": {
"statements": 95,
"branches": 93,
"functions": 94,
"lines": 94
},
"./packages/app/src/": {
"statements": 93,
"branches": 100,
"functions": 83,
"lines": 92
},
"./packages/backend/src/": {
"statements": 100,
"branches": 100,
"functions": 100,
"lines": 100
}
}
}
}
58 changes: 58 additions & 0 deletions packages/app/e2e-tests/fixtures/multi-tier.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "multi-tier",
"resources": [
{
"id": "/planes/radius/local/resourceGroups/demo/providers/Applications.Core/gateways/edge",
"name": "edge",
"type": "Applications.Core/gateways",
"provider": "radius",
"provisioningState": "Succeeded"
},
{
"id": "/planes/radius/local/resourceGroups/demo/providers/Applications.Core/containers/frontend",
"name": "frontend",
"type": "Applications.Core/containers",
"provider": "radius",
"provisioningState": "Succeeded",
"connections": [
{
"id": "/planes/radius/local/resourceGroups/demo/providers/Applications.Core/gateways/edge",
"name": "edge",
"type": "Applications.Core/gateways",
"provider": "radius",
"direction": "Inbound"
},
{
"id": "/planes/radius/local/resourceGroups/demo/providers/Applications.Core/containers/backend",
"name": "backend",
"type": "Applications.Core/containers",
"provider": "radius",
"direction": "Outbound"
}
]
},
{
"id": "/planes/radius/local/resourceGroups/demo/providers/Applications.Core/containers/backend",
"name": "backend",
"type": "Applications.Core/containers",
"provider": "radius",
"provisioningState": "Succeeded",
"connections": [
{
"id": "/planes/radius/local/resourceGroups/demo/providers/Applications.Datastores/redisCaches/cache",
"name": "cache",
"type": "Applications.Datastores/redisCaches",
"provider": "radius",
"direction": "Outbound"
}
]
},
{
"id": "/planes/radius/local/resourceGroups/demo/providers/Applications.Datastores/redisCaches/cache",
"name": "cache",
"type": "Applications.Datastores/redisCaches",
"provider": "radius",
"provisioningState": "Succeeded"
}
]
}
229 changes: 229 additions & 0 deletions packages/app/e2e-tests/radiusGraph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { expect, Page, Route, test } from '@playwright/test';
import multiTier from './fixtures/multi-tier.json';

const applicationId = (
namespace: 'Applications.Core' | 'Radius.Core',
name: string,
) =>
`/planes/radius/local/resourceGroups/demo/providers/${namespace}/applications/${name}`;

const application = (
namespace: 'Applications.Core' | 'Radius.Core',
name: string,
) => ({
id: applicationId(namespace, name),
name,
type: `${namespace}/applications`,
location: 'global',
properties: {
environment:
'/planes/radius/local/resourceGroups/demo/providers/Applications.Core/environments/demo',
},
});

const applications = [
application('Applications.Core', 'legacy-app'),
application('Radius.Core', 'radius-app'),
];

const environment = {
id: '/planes/radius/local/resourceGroups/demo/providers/Applications.Core/environments/demo',
name: 'demo',
type: 'Applications.Core/environments',
location: 'global',
properties: {},
};

const resourceType = (namespace: string) => ({
Name: 'applications',
Description: 'Application resource type',
ResourceProviderNamespace: namespace,
APIVersions: { '2025-01-01': {} },
APIVersionList: ['2025-01-01'],
});

async function fulfillRadiusRequest(route: Route) {
const url = decodeURIComponent(route.request().url());

if (url.endsWith('/api/kubernetes/clusters')) {
await route.fulfill({
json: {
items: [{ name: 'e2e-cluster', authProvider: 'serviceAccount' }],
},
});
return;
}

if (!url.includes('/api/kubernetes/proxy/')) {
await route.continue();
return;
}

if (url.includes('/getGraph?')) {
await route.fulfill({ json: multiTier });
return;
}

const namespace = url.includes('/Radius.Core/')
? 'Radius.Core'
: 'Applications.Core';
if (url.includes('/resourceTypes/applications?')) {
await route.fulfill({ json: resourceType(namespace) });
return;
}

const matchingApplication = applications.find(item => url.includes(item.id));
if (matchingApplication) {
await route.fulfill({ json: matchingApplication });
return;
}

if (url.includes(environment.id)) {
await route.fulfill({ json: environment });
return;
}

if (url.includes('/applications?')) {
await route.fulfill({
json: {
value: applications.filter(item => item.type.startsWith(namespace)),
},
});
return;
}

if (url.includes('/environments?')) {
await route.fulfill({ json: { value: [environment] } });
return;
}

await route.fulfill({ json: { value: [] } });
}

async function enterDashboard(page: Page) {
page.on('dialog', dialog => dialog.accept());
await page.goto('/applications');

const enter = page.getByRole('button', { name: 'Enter' });
if (await enter.isVisible({ timeout: 5_000 }).catch(() => false)) {
await enter.click();
}
await expect(
page.getByRole('heading', { name: 'Applications', level: 1 }),
).toBeVisible({ timeout: 15_000 });
}

test.describe('Radius application graph journey', () => {
test.beforeEach(async ({ page }) => {
await page.route('**/api/kubernetes/**', fulfillRadiusRequest);
});

test('PU-28: resolves every lazy plugin extension through its host route', async ({
page,
}) => {
await enterDashboard(page);

const routes = [
['/applications', 'Applications'],
['/environments', 'Environments'],
['/recipes', 'Recipes'],
['/resource-types', 'Resource Types'],
['/resources', 'Resources'],
['/resource-types/Radius.Core/applications', 'applications'],
[
'/resources/demo/Applications.Core/applications/legacy-app/overview',
'Resource',
],
[
'/environments/demo/Applications.Core/environments/demo/overview',
'Environment',
],
] as const;

for (const [route, heading] of routes) {
await page.goto(route);
await expect(
page.getByRole('heading', { name: heading, level: 1 }),
).toBeVisible({ timeout: 15_000 });
}
});

test('E2E-03 / GU-12 / GU-13: navigates from both application namespaces to the real graph', async ({
page,
}) => {
await enterDashboard(page);

for (const item of applications) {
const applicationLink = page.getByRole('link', { name: item.name });
await expect(applicationLink).toBeVisible();
await applicationLink.click();
await page.getByRole('tab', { name: 'App Graph' }).click();

await expect(
page.getByRole('button', {
name: /^frontend/,
}),
).toBeVisible();
await expect(
page.getByRole('button', {
name: /^Edge from .*backend.* to .*frontend/,
}),
).toBeVisible();

await page.goto('/applications');
}
});

test('E2E-10: direct-link refresh preserves the rendered application graph', async ({
page,
}) => {
await enterDashboard(page);
await page.goto(
'/resources/demo/Applications.Core/applications/legacy-app/application',
);
await expect(
page.getByRole('button', {
name: /^frontend/,
}),
).toBeVisible();

await page.reload();

await expect(
page.getByRole('button', {
name: /^frontend/,
}),
).toBeVisible();
});

// KNOWN-DEFECT (#370): the visible error needs an accessible retry action.
test('GU-16 / ER-10: KNOWN-DEFECT an unavailable graph request renders an error without retry', async ({
page,
}) => {
await page.route(/\/api\/kubernetes\/proxy\/.*\/getGraph\?/, route =>
route.fulfill({ status: 503, body: 'Service Unavailable' }),
);
await enterDashboard(page);
await page.goto(
'/resources/demo/Applications.Core/applications/legacy-app/application',
);

await expect(page.getByRole('alert')).toContainText('Request failed: 503');
await expect(page.locator('.react-flow')).toHaveCount(0);
});

test('ER-09: a graph request timeout renders its distinct timeout error', async ({
page,
}) => {
await page.route(/\/api\/kubernetes\/proxy\/.*\/getGraph\?/, () => {});
await enterDashboard(page);
await page.goto(
'/resources/demo/Applications.Core/applications/legacy-app/application',
);

await expect(page.getByRole('alert')).toContainText('timed out', {
timeout: 15_000,
});
await expect(page.locator('.react-flow')).toHaveCount(0);
});
});
1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
},
"devDependencies": {
"@backstage/cli-defaults": "^0.1.5",
"@backstage/config": "^1.3.8",
"@backstage/test-utils": "^1.7.21",
"@playwright/test": "^1.62.1",
"@testing-library/dom": "^10.4.1",
Expand Down
Loading
Loading