Segment-bound doctor and scheduler frontdoor chunks
This commit is contained in:
@@ -136,8 +136,9 @@ flows, Rekor, and static/platform bootstrap assets).
|
||||
The local route table also carries a small set of explicit precedence rules that must stay ahead of the
|
||||
generic `^/api/v1/{service}` matcher. Platform-owned surfaces such as `/api/v1/aoc/*` and
|
||||
`/api/v1/administration/*` resolve directly to `platform`, and browser compatibility prefixes such as
|
||||
`/doctor/*` and `/scheduler/*` strip the frontdoor prefix before dispatch so the target services still
|
||||
receive their canonical `/api/v1/doctor/*` and `/api/v1/scheduler/*` paths.
|
||||
`/doctor/*` and `/scheduler/*` are segment-bound before they strip the frontdoor prefix for dispatch.
|
||||
That keeps the target services on their canonical `/api/v1/doctor/*` and `/api/v1/scheduler/*` paths
|
||||
without hijacking frontend assets like `doctor.routes-*.js` or `scheduler-ops.routes-*.js`.
|
||||
|
||||
```bash
|
||||
# Default frontdoor route table
|
||||
|
||||
@@ -105,8 +105,8 @@
|
||||
{ "Type": "Microservice", "Path": "^/api/admin/plans(.*)", "IsRegex": true, "TranslatesTo": "http://registry-token.stella-ops.local/api/admin/plans$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/admin(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/admin$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/analytics(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/analytics$1" },
|
||||
{ "Type": "Microservice", "Path": "^/scheduler(.*)", "IsRegex": true, "TranslatesTo": "http://scheduler.stella-ops.local$1" },
|
||||
{ "Type": "Microservice", "Path": "^/doctor(.*)", "IsRegex": true, "TranslatesTo": "http://doctor.stella-ops.local$1" },
|
||||
{ "Type": "Microservice", "Path": "^/scheduler(?=/|$)(.*)", "IsRegex": true, "TranslatesTo": "http://scheduler.stella-ops.local$1" },
|
||||
{ "Type": "Microservice", "Path": "^/doctor(?=/|$)(.*)", "IsRegex": true, "TranslatesTo": "http://doctor.stella-ops.local$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/orchestrator(.*)", "IsRegex": true, "TranslatesTo": "http://orchestrator.stella-ops.local/api/orchestrator$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/jobengine(.*)", "IsRegex": true, "TranslatesTo": "http://orchestrator.stella-ops.local/api/jobengine$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/scheduler(.*)", "IsRegex": true, "TranslatesTo": "http://scheduler.stella-ops.local/api/scheduler$1" },
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Sprint 20260310-021 - Router Frontdoor Segment Bound Scheduler Doctor Chunks
|
||||
|
||||
## Topic & Scope
|
||||
- Repair the remaining live frontdoor regressions where newly-added `/doctor` and `/scheduler` compatibility prefixes capture Angular lazy chunks instead of only owning the actual URL segment.
|
||||
- Revalidate the scratch stack with Playwright after the gateway config change so canonical route coverage can move past router dispatch defects.
|
||||
- Working directory: `src/Router`.
|
||||
- Allowed coordination edits: `devops/compose/router-gateway-local.json`, `docs/modules/router/architecture.md`, `devops/compose/README.md`, `docs/implplan/SPRINT_20260310_021_Router_frontdoor_segment_bound_scheduler_doctor_chunks.md`.
|
||||
- Expected evidence: focused router tests, live chunk/backend probes, and a rerun of the canonical Playwright sweep.
|
||||
|
||||
## Dependencies & Concurrency
|
||||
- Depends on `SPRINT_20260310_020_Router_frontdoor_route_boundary_and_service_prefix_repair.md`, which introduced the shell compatibility prefixes that exposed the lazy-chunk collision.
|
||||
- Safe parallelism: stay inside router config/tests/docs while the frontend route layer remains untouched.
|
||||
|
||||
## Documentation Prerequisites
|
||||
- `AGENTS.md`
|
||||
- `src/Router/AGENTS.md`
|
||||
- `src/Router/StellaOps.Gateway.WebService/AGENTS.md`
|
||||
- `src/Router/__Tests/StellaOps.Gateway.WebService.Tests/AGENTS.md`
|
||||
- `docs/modules/router/architecture.md`
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### ROUTER-SEGMENT-001 - Segment-bound doctor and scheduler frontdoor prefixes
|
||||
Status: DONE
|
||||
Dependency: none
|
||||
Owners: Developer, QA
|
||||
Task description:
|
||||
- Change the frontdoor `/doctor` and `/scheduler` regexes so they match only the route segment and no longer own static chunk filenames such as `doctor.routes-*.js` and `scheduler-ops.routes-*.js`.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Gateway config uses segment-bound regexes for both prefixes.
|
||||
- [x] Focused route-table tests lock the exact regexes.
|
||||
- [x] Resolver tests prove static chunks still resolve to SPA/static files.
|
||||
|
||||
### ROUTER-SEGMENT-002 - Reverify the live scratch frontdoor and Playwright sweep
|
||||
Status: DONE
|
||||
Dependency: ROUTER-SEGMENT-001
|
||||
Owners: QA
|
||||
Task description:
|
||||
- Restart `router-gateway`, verify the affected chunk URLs and API prefixes live, and rerun the canonical Playwright route sweep on `https://stella-ops.local`.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Live chunk requests for doctor and scheduler return frontend assets instead of `503`.
|
||||
- [x] Live backend requests through `/doctor/*` and `/scheduler/*` still reach the authenticated services.
|
||||
- [x] Canonical Playwright sweep no longer fails on `/ops/operations/doctor` or `/ops/operations/scheduler`.
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
| 2026-03-10 | Sprint created after the canonical Playwright sweep isolated two remaining failures. Root cause: the new `/doctor` and `/scheduler` frontdoor regexes were broad enough to capture lazy chunk files and return `503` on dynamic imports. | Developer |
|
||||
| 2026-03-10 | Tightened the doctor and scheduler regexes to `(?=/|$)` segment-bound forms, added route-table/resolver/middleware coverage, and reran the focused router test project successfully (`273/273`). | Developer |
|
||||
| 2026-03-10 | Recycled `router-gateway`, confirmed doctor/scheduler API prefixes still return authenticated backend responses, confirmed the previously failing lazy chunk URLs now return `200 text/javascript`, and reran the canonical Playwright sweep cleanly at `111/111` passed. | QA |
|
||||
|
||||
## Decisions & Risks
|
||||
- Decision: root-prefix compatibility routes must follow the same segment-bound rule as `/policy` whenever the SPA emits chunk names with a shared textual prefix.
|
||||
- Risk: if additional frontdoor root prefixes are added without the boundary rule, similar lazy-chunk outages can reappear. Mitigation: keep resolver tests for static-chunk collisions alongside route-table changes.
|
||||
|
||||
## Next Checkpoints
|
||||
- 2026-03-10: rerun the live canonical sweep and confirm whether any non-router failures remain.
|
||||
@@ -89,7 +89,7 @@ Reverse proxy is reserved for external/bootstrap surfaces such as OIDC browser f
|
||||
|
||||
Regex microservice routes that own a root prefix must use a segment boundary when the same prefix can appear in static asset filenames. The local frontdoor uses `^/policy(?=/|$)(.*)` rather than `^/policy(.*)` so Angular chunks such as `/policy-decisioning.routes-*.js` stay on the SPA/static path instead of being misrouted to the Policy service.
|
||||
|
||||
Browser-facing compatibility prefixes that exist only at the frontdoor must strip that prefix before dispatching to the target microservice. Local compose keeps `/doctor/api/v1/doctor/*` and `/scheduler/api/v1/scheduler/*` for the shell, but the route table translates them to `http://doctor.stella-ops.local$1` and `http://scheduler.stella-ops.local$1` so the backend still receives its canonical `/api/v1/<service>/*` path.
|
||||
Browser-facing compatibility prefixes that exist only at the frontdoor must also use segment boundaries and strip that prefix before dispatching to the target microservice. Local compose keeps `/doctor/api/v1/doctor/*` and `/scheduler/api/v1/scheduler/*` for the shell, but the route table translates `^/doctor(?=/|$)(.*)` and `^/scheduler(?=/|$)(.*)` to `http://doctor.stella-ops.local$1` and `http://scheduler.stella-ops.local$1` so the backend still receives its canonical `/api/v1/<service>/*` path without stealing SPA chunks such as `doctor.routes-*.js`.
|
||||
|
||||
### Pipeline Order
|
||||
|
||||
|
||||
@@ -13,3 +13,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| RGH-02 | DONE | 2026-03-04: Expanded approved auth passthrough prefixes (`/authority`, `/doctor`, `/api`) to unblock authenticated gateway routes used by Audit Log UI E2E. |
|
||||
| RGH-03 | DONE | 2026-03-05: Aligned `/api/v1/search*` and `/api/v1/advisory-ai*` route translations to AdvisoryAI `/v1/*`, added compose/runtime parity safeguards, and verified setup smoke coverage. |
|
||||
| RGH-04 | DONE | 2026-03-10: Tightened frontdoor route boundaries for `/policy`, restored explicit platform ownership for `/api/v1/aoc*` and `/api/v1/administration*`, and stripped `/doctor`/`/scheduler` shell prefixes before microservice dispatch. |
|
||||
| RGH-05 | DONE | 2026-03-10: Added segment boundaries to `/doctor` and `/scheduler` frontdoor prefixes so lazy web chunks stay on static hosting while compatibility API prefixes still dispatch to the correct microservice. |
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
{ "Type": "Microservice", "Path": "^/api/admin/plans(.*)", "IsRegex": true, "TranslatesTo": "http://registry-token.stella-ops.local/api/admin/plans$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/admin(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/admin$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/analytics(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/analytics$1" },
|
||||
{ "Type": "Microservice", "Path": "^/scheduler(.*)", "IsRegex": true, "TranslatesTo": "http://scheduler.stella-ops.local$1" },
|
||||
{ "Type": "Microservice", "Path": "^/doctor(.*)", "IsRegex": true, "TranslatesTo": "http://doctor.stella-ops.local$1" },
|
||||
{ "Type": "Microservice", "Path": "^/scheduler(?=/|$)(.*)", "IsRegex": true, "TranslatesTo": "http://scheduler.stella-ops.local$1" },
|
||||
{ "Type": "Microservice", "Path": "^/doctor(?=/|$)(.*)", "IsRegex": true, "TranslatesTo": "http://doctor.stella-ops.local$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/orchestrator(.*)", "IsRegex": true, "TranslatesTo": "http://orchestrator.stella-ops.local/api/orchestrator$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/jobengine(.*)", "IsRegex": true, "TranslatesTo": "http://orchestrator.stella-ops.local/api/jobengine$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/scheduler(.*)", "IsRegex": true, "TranslatesTo": "http://scheduler.stella-ops.local/api/scheduler$1" },
|
||||
|
||||
@@ -19,8 +19,8 @@ public sealed class GatewayRouteSearchMappingsTests
|
||||
("^/api/v2/security(.*)", "http://platform.stella-ops.local/api/v2/security$1", "Microservice", true),
|
||||
("^/api/v2/topology(.*)", "http://platform.stella-ops.local/api/v2/topology$1", "Microservice", true),
|
||||
("^/api/v2/integrations(.*)", "http://platform.stella-ops.local/api/v2/integrations$1", "Microservice", true),
|
||||
("^/scheduler(.*)", "http://scheduler.stella-ops.local$1", "Microservice", true),
|
||||
("^/doctor(.*)", "http://doctor.stella-ops.local$1", "Microservice", true),
|
||||
("^/scheduler(?=/|$)(.*)", "http://scheduler.stella-ops.local$1", "Microservice", true),
|
||||
("^/doctor(?=/|$)(.*)", "http://doctor.stella-ops.local$1", "Microservice", true),
|
||||
("^/api/jobengine(.*)", "http://orchestrator.stella-ops.local/api/jobengine$1", "Microservice", true),
|
||||
("^/api/scheduler(.*)", "http://scheduler.stella-ops.local/api/scheduler$1", "Microservice", true)
|
||||
];
|
||||
@@ -90,7 +90,7 @@ public sealed class GatewayRouteSearchMappingsTests
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RouteConfigPaths))]
|
||||
public void RouteTable_UsesSegmentBoundaryForPolicyRootRoute(string configRelativePath)
|
||||
public void RouteTable_UsesSegmentBoundariesForFrontdoorRootPrefixes(string configRelativePath)
|
||||
{
|
||||
var repoRoot = FindRepositoryRoot();
|
||||
var configPath = Path.Combine(repoRoot, configRelativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
@@ -113,9 +113,32 @@ public sealed class GatewayRouteSearchMappingsTests
|
||||
route.TryGetProperty("IsRegex", out var isRegex) && isRegex.GetBoolean()))
|
||||
.ToList();
|
||||
|
||||
var route = routes.SingleOrDefault(candidate => string.Equals(candidate.TranslatesTo, "http://policy-gateway.stella-ops.local/policy$1", StringComparison.Ordinal));
|
||||
Assert.True(route is not null, $"Missing policy root route in {configRelativePath}.");
|
||||
Assert.Equal("^/policy(?=/|$)(.*)", route!.Path);
|
||||
AssertSegmentBoundRoute(
|
||||
routes,
|
||||
"http://policy-gateway.stella-ops.local/policy$1",
|
||||
"^/policy(?=/|$)(.*)",
|
||||
configRelativePath);
|
||||
AssertSegmentBoundRoute(
|
||||
routes,
|
||||
"http://scheduler.stella-ops.local$1",
|
||||
"^/scheduler(?=/|$)(.*)",
|
||||
configRelativePath);
|
||||
AssertSegmentBoundRoute(
|
||||
routes,
|
||||
"http://doctor.stella-ops.local$1",
|
||||
"^/doctor(?=/|$)(.*)",
|
||||
configRelativePath);
|
||||
}
|
||||
|
||||
private static void AssertSegmentBoundRoute(
|
||||
IEnumerable<RouteEntry> routes,
|
||||
string expectedTarget,
|
||||
string expectedPath,
|
||||
string configRelativePath)
|
||||
{
|
||||
var route = routes.SingleOrDefault(candidate => string.Equals(candidate.TranslatesTo, expectedTarget, StringComparison.Ordinal));
|
||||
Assert.True(route is not null, $"Missing route for {expectedTarget} in {configRelativePath}.");
|
||||
Assert.Equal(expectedPath, route!.Path);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
@@ -391,8 +391,8 @@ public sealed class RouteDispatchMiddlewareMicroserviceTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/doctor/api/v1/doctor/checks", @"^/doctor(.*)", "http://doctor.stella-ops.local$1", "doctor", "/api/v1/doctor/checks")]
|
||||
[InlineData("/scheduler/api/v1/scheduler/runs", @"^/scheduler(.*)", "http://scheduler.stella-ops.local$1", "scheduler", "/api/v1/scheduler/runs")]
|
||||
[InlineData("/doctor/api/v1/doctor/checks", @"^/doctor(?=/|$)(.*)", "http://doctor.stella-ops.local$1", "doctor", "/api/v1/doctor/checks")]
|
||||
[InlineData("/scheduler/api/v1/scheduler/runs", @"^/scheduler(?=/|$)(.*)", "http://scheduler.stella-ops.local$1", "scheduler", "/api/v1/scheduler/runs")]
|
||||
public async Task InvokeAsync_ServiceRootPrefixRoute_StripsFrontdoorPrefixBeforeDispatch(
|
||||
string requestPath,
|
||||
string routePath,
|
||||
|
||||
@@ -113,6 +113,45 @@ public sealed class StellaOpsRouteResolverTests
|
||||
Assert.Equal("/overview", policyChildResult.RegexMatch!.Groups[1].Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"^/doctor(?=/|$)(.*)", "http://doctor.stella-ops.local$1", "/doctor.routes-JHTKGEQ6.js", StellaOpsRouteType.StaticFiles, null)]
|
||||
[InlineData(@"^/doctor(?=/|$)(.*)", "http://doctor.stella-ops.local$1", "/doctor/api/v1/doctor/checks", StellaOpsRouteType.Microservice, "/api/v1/doctor/checks")]
|
||||
[InlineData(@"^/scheduler(?=/|$)(.*)", "http://scheduler.stella-ops.local$1", "/scheduler-ops.routes-5SHXET2O.js", StellaOpsRouteType.StaticFiles, null)]
|
||||
[InlineData(@"^/scheduler(?=/|$)(.*)", "http://scheduler.stella-ops.local$1", "/scheduler/api/v1/scheduler/runs", StellaOpsRouteType.Microservice, "/api/v1/scheduler/runs")]
|
||||
public void Resolve_SegmentBoundFrontdoorServiceRoots_DoNotCaptureStaticChunks(
|
||||
string routePath,
|
||||
string translatesTo,
|
||||
string requestPath,
|
||||
StellaOpsRouteType expectedType,
|
||||
string? expectedCapture)
|
||||
{
|
||||
var resolver = new StellaOpsRouteResolver(
|
||||
[
|
||||
MakeRoute(
|
||||
routePath,
|
||||
isRegex: true,
|
||||
translatesTo: translatesTo),
|
||||
MakeRoute(
|
||||
"/",
|
||||
type: StellaOpsRouteType.StaticFiles,
|
||||
translatesTo: "/app/wwwroot")
|
||||
]);
|
||||
|
||||
var result = resolver.Resolve(new PathString(requestPath));
|
||||
|
||||
Assert.NotNull(result.Route);
|
||||
Assert.Equal(expectedType, result.Route!.Type);
|
||||
|
||||
if (expectedCapture is null)
|
||||
{
|
||||
Assert.Null(result.RegexMatch);
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.NotNull(result.RegexMatch);
|
||||
Assert.Equal(expectedCapture, result.RegexMatch!.Groups[1].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_NoMatch_ReturnsNull()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user