58 KiB
React Pro Integration Foundation Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a reproducible React Pro engineering baseline and a contract-driven BuildFlow transport layer that proves login, refresh, current-user, menu, and access-code integration against the real .NET backend.
Architecture: Keep generated OpenAPI code under src/services/buildflow/generated/, place stable domain adapters and special-protocol clients beside it, and centralize token, envelope, error, and refresh behavior in one Umi request configuration. Run the full Host against pinned local infrastructure for backend contract tests, API smoke tests, and Playwright, while preserving the backend's default-deny authorization policy.
Tech Stack: React 19, Umi Max 4, Ant Design Pro 6, TypeScript 6, Jest 30, Playwright 1.61, @umijs/openapi 1.14, .NET 10, FastEndpoints 8, xUnit, Docker Compose, PostgreSQL, MongoDB, Redis, RabbitMQ, MinIO.
Repository Boundaries
- Frontend working directory:
/Users/wen/project/rag/build-flow.admin - Backend working directory:
/Users/wen/project/rag/build-flow.api - Approved design:
docs/superpowers/specs/2026-07-20-react-pro-integration-foundation-design.md - The backend repository is clean at plan creation. The frontend directory has no
.git; Task 1 creates a baseline commit before changing user source. - Do not push either repository. Backend commits must contain only files from the current task.
File Responsibility Map
Frontend
package.json,package-lock.json: one npm dependency graph and all verification commands.tsconfig.json,biome.json,jest.config.ts,tests/setupTests.jsx: main-app compilation and test boundaries.config/openapi/buildflow.json: committed, normalized backend contract snapshot.scripts/openapi-utils.mjs: validation and stable JSON helpers shared by sync/check tests.scripts/sync-openapi.mjs: fetch and atomically update the snapshot.scripts/generate-openapi.mjs: generate onlysrc/services/buildflow/generated/.scripts/generate-template-openapi.mjs: restore the untouched Ant Design Pro demo service directory from its preserved snapshot.scripts/check-openapi.mjs: regenerate in a temporary directory and compare with committed output.src/services/buildflow/transport/*: token storage, normalized errors, refresh de-duplication, interceptors, and the generator-facingrequest()export.src/services/buildflow/adapters/auth.ts: login/logout/current-user/menu/access-code boundary.src/services/buildflow/contracts.ts: stable UI-facing authentication and menu contracts.src/services/buildflow/download.ts,upload.ts,sse.ts,signalr.ts: special-protocol boundaries.src/app.tsx,src/access.ts,src/utils/menuTransform.tsx: session bootstrap, authorization, and menu normalization.src/pages/user/login/index.tsx: real account login only; no demo mobile/social actions.src/components/RightContent/AvatarDropdown.tsx: real backend logout and local-state cleanup.scripts/api-smoke.mjs: credential-safe real API verification.playwright.config.ts,tests/e2e/auth.spec.ts: browser session and refresh verification.docs/BACKEND_INTEGRATION.md,docs/INTEGRATION_SOP.md: authoritative operator workflow.
Backend
src/BuildFlow.Host/Program.cs: Testing configuration precedence andWebApplicationFactoryentry point.src/BuildFlow.Host/appsettings.Testing.json: non-secret local contract environment.src/BuildFlow.Host/BuildFlowHostModule.cs: health endpoint, Swagger placement, validation envelope.src/shared/BuildFlow.Shared/Api/ApiErrorMapper.cs: one shared semantic-code to HTTP mapping.src/shared/BuildFlow.Shared/Api/GlobalExceptionMiddleware.cs: use the shared mapper.src/shared/BuildFlow.Shared/Api/GlobalExceptionHandler.cs: use the same mapper for FastEndpoints.src/BuildFlow.Host/AggregateExceptionHandler.cs: compose shared mapping with file-specific exceptions.tests/infra/docker-compose.yml,tests/infra/postgres/init.sql: pinned local dependencies on non-conflicting ports.tests/BuildFlow.Host.Contract.Tests/*: real Host contract tests.tests/BuildFlow.IM.Integration.Tests/ImServerFixture.cs,tests/BuildFlow.IM.Tests/Chat/SendMessageCommandHandlerTests.cs: use the shared test-infrastructure ports instead of hard-coded machine ports.scripts/test-infra.sh,scripts/testing-host.sh: repeatable infrastructure and real HTTP lifecycle.run-dev.sh: one documented local API port, 5667.
Task 1: Establish the React Pro Git and npm Baseline
Files:
-
Modify:
/Users/wen/project/rag/build-flow.admin/.gitignore -
Modify:
/Users/wen/project/rag/build-flow.admin/package.json -
Create:
/Users/wen/project/rag/build-flow.admin/package-lock.json -
Delete:
/Users/wen/project/rag/build-flow.admin/pnpm-lock.yaml -
Delete:
/Users/wen/project/rag/build-flow.admin/pnpm-workspace.yaml -
Modify:
/Users/wen/project/rag/build-flow.admin/tsconfig.json -
Modify:
/Users/wen/project/rag/build-flow.admin/biome.json -
Modify:
/Users/wen/project/rag/build-flow.admin/jest.config.ts -
Step 1: Create an unchanged frontend baseline commit
Run:
cd /Users/wen/project/rag/build-flow.admin
git init -b main
git add .
git commit -m "chore: establish react pro baseline"
Expected: the commit succeeds without tracking node_modules, dist, .umi, .umi-production, local environment files, or screenshots.
- Step 2: Record the package-manager failure before changing files
Run:
test ! -f package-lock.json
npm test -- --runInBand
Expected: the first command succeeds; Jest fails while resolving node_modules/@utoo/pack/package.json.
- Step 3: Replace the pnpm installation with direct npm dependencies
Run:
rm -rf node_modules
rm pnpm-lock.yaml pnpm-workspace.yaml
npm install
npm install --save-exact @microsoft/signalr@10.0.0
npm install --save-dev --save-exact @playwright/test@1.61.1 @types/geojson@7946.0.16 @umijs/openapi@1.14.1 @utoo/pack@1.4.27
Expected: npm creates package-lock.json; packages imported directly by configuration or application code are direct dependencies. The Umi OpenAPI plugin remains installed until Task 5 removes its configuration.
- Step 4: Restrict TypeScript to the main React application
Replace the broad include in tsconfig.json with:
"include": [
"config/**/*.ts",
"jest.config.ts",
"src/**/*.d.ts",
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"cloudflare-worker",
"coverage",
"dist",
"node_modules",
"src/.umi",
"src/.umi-*"
]
Expected: the root tsc command no longer compiles the standalone Hono worker.
- Step 5: Make Biome lint handwritten BuildFlow services but not generated services
Update biome.json so files.includes contains these exclusions instead of excluding all of src/services:
"!cloudflare-worker/**",
"!src/services/ant-design-pro/**",
"!src/services/buildflow/generated/**"
Change the rules preset from:
"recommended": true
to:
"preset": "recommended"
Expected: handwritten files under src/services/buildflow/ are linted and Biome no longer emits the deprecated-preset notice.
- Step 6: Make Jest resolve direct packages through Node
Use this implementation in jest.config.ts:
const readPkgVersion = (pkg: string) =>
JSON.parse(
readFileSync(require.resolve(`${pkg}/package.json`), 'utf-8'),
).version;
Keep __UMI_VERSION__ and __UTOO_VERSION__, but do not construct paths beneath node_modules manually.
- Step 7: Verify a clean npm install and configuration load
Run:
rm -rf node_modules
npm ci
npm test -- --listTests
npm run tsc
Expected: npm ci succeeds from package-lock.json; Jest lists the login test instead of failing in configuration; TypeScript no longer reports Hono or GeoJSON declaration errors.
- Step 8: Commit the npm baseline
git add .gitignore package.json package-lock.json tsconfig.json biome.json jest.config.ts pnpm-lock.yaml pnpm-workspace.yaml
git commit -m "build: standardize react pro on npm"
Task 2: Make the Existing Frontend Lint and Build Baseline Green
Files:
-
Modify:
/Users/wen/project/rag/build-flow.admin/src/utils/menuTransform.tsx -
Modify:
/Users/wen/project/rag/build-flow.admin/src/pages/table-list/components/UpdateForm.tsx -
Step 1: Reproduce the handwritten lint failures
Run:
npm run lint
Expected: Biome reports string concatenation in menuTransform.tsx and the unused FormValueType in UpdateForm.tsx.
- Step 2: Apply the minimal lint fixes
Use this path normalization statement:
path = `${parentPath.replace(/\/$/, '')}/${path}`;
Delete the unused FormValueType declaration from UpdateForm.tsx; do not change the form's runtime behavior.
- Step 3: Verify the existing application baseline
Run:
npm run lint
npx antd lint ./src
npm run build
Expected: all commands exit 0. Existing antd context warnings in template-only pages may still be reported as warnings, but the BuildFlow transport added later must introduce none.
- Step 4: Commit the baseline fixes
git add src/utils/menuTransform.tsx src/pages/table-list/components/UpdateForm.tsx
git commit -m "fix: restore react pro quality gates"
Task 3: Add Isolated Backend Test Infrastructure and a Health Contract
Files:
-
Create:
tests/infra/docker-compose.yml -
Create:
tests/infra/postgres/init.sql -
Create:
scripts/test-infra.sh -
Create:
src/BuildFlow.Host/appsettings.Testing.json -
Modify:
src/BuildFlow.Host/Program.cs -
Modify:
src/BuildFlow.Host/BuildFlowHostModule.cs -
Create:
tests/BuildFlow.Host.Contract.Tests/BuildFlow.Host.Contract.Tests.csproj -
Create:
tests/BuildFlow.Host.Contract.Tests/BuildFlowHostFactory.cs -
Create:
tests/BuildFlow.Host.Contract.Tests/HostContractCollection.cs -
Create:
tests/BuildFlow.Host.Contract.Tests/HostStartupContractTests.cs -
Modify:
tests/BuildFlow.IM.Integration.Tests/ImServerFixture.cs -
Modify:
tests/BuildFlow.IM.Tests/Chat/SendMessageCommandHandlerTests.cs -
Modify:
BuildFlow.slnx -
Step 1: Create pinned local infrastructure on dedicated ports
Create tests/infra/docker-compose.yml with these services and host ports:
services:
postgres:
image: pgvector/pgvector:0.8.0-pg17
environment:
POSTGRES_USER: buildflow
POSTGRES_PASSWORD: buildflow
POSTGRES_DB: postgres
ports: ["55432:5432"]
volumes:
- ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U buildflow -d postgres"]
interval: 2s
timeout: 3s
retries: 30
mongo:
image: mongo:8.0.13
environment:
MONGO_INITDB_ROOT_USERNAME: buildflow
MONGO_INITDB_ROOT_PASSWORD: buildflow
ports: ["57017:27017"]
healthcheck:
test: ["CMD", "mongosh", "--host", "localhost", "--username", "buildflow", "--password", "buildflow", "--authenticationDatabase", "admin", "--quiet", "--eval", "db.adminCommand('ping')"]
interval: 2s
timeout: 5s
retries: 30
redis:
image: redis:8.0.3-alpine
command: ["redis-server", "--requirepass", "buildflow"]
ports: ["56379:6379"]
healthcheck:
test: ["CMD", "redis-cli", "-a", "buildflow", "ping"]
interval: 2s
timeout: 3s
retries: 30
rabbitmq:
image: rabbitmq:4.1.2-management-alpine
environment:
RABBITMQ_DEFAULT_USER: buildflow
RABBITMQ_DEFAULT_PASS: buildflow
ports: ["56792:5672", "56793:15672"]
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 3s
timeout: 5s
retries: 30
minio:
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
command: server /data --console-address :9001
environment:
MINIO_ROOT_USER: buildflow
MINIO_ROOT_PASSWORD: buildflow-test-secret
ports: ["59000:9000", "59001:9001"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 3s
timeout: 5s
retries: 30
Create tests/infra/postgres/init.sql with one statement per database:
CREATE DATABASE rag;
CREATE DATABASE file_system;
CREATE DATABASE im_system;
CREATE DATABASE im_system_test;
CREATE DATABASE workflow;
CREATE DATABASE platform;
CREATE DATABASE monitoring;
\connect rag
CREATE EXTENSION IF NOT EXISTS vector;
\connect postgres
- Step 2: Add one infrastructure lifecycle script
Create scripts/test-infra.sh:
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
COMPOSE_FILE="$ROOT_DIR/tests/infra/docker-compose.yml"
case "${1:-}" in
up)
docker compose -f "$COMPOSE_FILE" up -d --wait
;;
down)
docker compose -f "$COMPOSE_FILE" down -v
;;
*)
echo "usage: $0 <up|down>" >&2
exit 2
;;
esac
Run chmod +x scripts/test-infra.sh before the first invocation.
- Step 3: Add the non-secret Testing configuration
Create src/BuildFlow.Host/appsettings.Testing.json with these effective values:
{
"Kestrel": { "Endpoints": { "Http": { "Url": "http://127.0.0.1:5667" } } },
"Consul": { "Token": "" },
"ConnectionStrings": {
"Default": "Host=127.0.0.1;Port=55432;Username=buildflow;Password=buildflow",
"MongoDb": "mongodb://buildflow:buildflow@127.0.0.1:57017/im_system?authSource=admin",
"Redis": "127.0.0.1:56379,password=buildflow,abortConnect=false"
},
"RabbitMq": {
"Host": "127.0.0.1",
"Port": 56792,
"Username": "buildflow",
"Password": "buildflow"
},
"S3": {
"Endpoint": "http://127.0.0.1:59000",
"AccessKey": "buildflow",
"SecretKey": "buildflow-test-secret",
"Region": "us-east-1"
},
"Jwt": {
"SigningKey": "BuildFlowTestingSigningKeyMustBeAtLeast32Chars!",
"Issuer": "buildflow-testing",
"Audience": "buildflow-admin",
"AccessTokenMinutes": 5,
"RefreshTokenDays": 1
},
"Cookie": { "Secure": false },
"OpenTelemetry": { "OtlpEndpoint": "http://127.0.0.1:4317" },
"Monitoring": { "Scheduler": { "StartupDelaySeconds": 3600 } }
}
- Step 4: Make Testing ignore local secrets and expose Program to tests
Change Program.cs to add secrets only outside Testing, then restore environment-variable precedence:
if (!builder.Environment.IsEnvironment("Testing"))
{
builder.Configuration.AddJsonFile(
"appsettings.Secrets.json",
optional: true,
reloadOnChange: true);
}
builder.Configuration.AddEnvironmentVariables();
Append:
public partial class Program { }
- Step 5: Write a failing anonymous health test
Create the contract test project with Microsoft.AspNetCore.Mvc.Testing 10.0.7, xUnit 2.9.2, FluentAssertions 6.12.2, and a project reference to BuildFlow.Host.
Use this factory:
public sealed class BuildFlowHostFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
}
}
Use a shared collection fixture and add this test:
[Collection(HostContractCollection.Name)]
public sealed class HostStartupContractTests(BuildFlowHostFactory factory)
{
[Fact]
public async Task Health_is_anonymous_and_business_api_remains_protected()
{
using var client = factory.CreateClient();
var health = await client.GetAsync("/health");
var protectedResponse = await client.GetAsync("/api/user/info");
health.StatusCode.Should().Be(HttpStatusCode.OK);
protectedResponse.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
}
- Step 6: Start infrastructure and verify the health test fails
Run:
bash scripts/test-infra.sh up
dotnet test tests/BuildFlow.Host.Contract.Tests --filter HostStartupContractTests
Expected: FAIL because /health is not mapped.
- Step 7: Add the health endpoint without weakening fallback authorization
In ConfigureServices, add:
services.AddHealthChecks();
During application initialization, map:
context.GetEndpointRouteBuilder()
.MapHealthChecks("/health")
.AllowAnonymous();
Expected: /health is public; /api/user/info stays 401.
- Step 8: Move hard-coded IM test connections to the isolated ports
Replace constants with environment-aware properties:
private static string PostgresConnection =>
Environment.GetEnvironmentVariable("BUILD_FLOW_TEST_POSTGRES")
?? "Host=localhost;Port=55432;Database=im_system_test;Username=buildflow;Password=buildflow";
private static string MongoConnection =>
Environment.GetEnvironmentVariable("BUILD_FLOW_TEST_MONGO")
?? "mongodb://buildflow:buildflow@localhost:57017/im_system_test?authSource=admin";
private static string RedisConnection =>
Environment.GetEnvironmentVariable("BUILD_FLOW_TEST_REDIS")
?? "localhost:56379,password=buildflow,abortConnect=false";
Use the same Mongo environment variable pattern in SendMessageCommandHandlerTests, changing only the database name to im_unit_test.
- Step 9: Verify startup and infrastructure-dependent tests
Run:
dotnet test tests/BuildFlow.Host.Contract.Tests --filter HostStartupContractTests
dotnet test tests/BuildFlow.IM.Integration.Tests --no-restore
dotnet test tests/BuildFlow.IM.Tests --no-restore
Expected: health/protection passes; the previous Mongo connection-refused failures are gone.
- Step 10: Add the test project to the solution and commit
Add the project under the /tests/ folder in BuildFlow.slnx, then run:
git add BuildFlow.slnx scripts/test-infra.sh src/BuildFlow.Host/Program.cs src/BuildFlow.Host/appsettings.Testing.json src/BuildFlow.Host/BuildFlowHostModule.cs tests/infra tests/BuildFlow.Host.Contract.Tests tests/BuildFlow.IM.Integration.Tests/ImServerFixture.cs tests/BuildFlow.IM.Tests/Chat/SendMessageCommandHandlerTests.cs
git commit -m "test: add isolated host contract environment"
Task 4: Standardize Backend Error, Validation, Swagger, and Auth Contracts
Files:
-
Create:
src/shared/BuildFlow.Shared/Api/ApiErrorMapper.cs -
Modify:
src/shared/BuildFlow.Shared/Api/GlobalExceptionMiddleware.cs -
Modify:
src/shared/BuildFlow.Shared/Api/GlobalExceptionHandler.cs -
Modify:
src/BuildFlow.Host/AggregateExceptionHandler.cs -
Modify:
src/BuildFlow.Host/BuildFlowHostModule.cs -
Delete:
src/shared/BuildFlow.Shared/Api/ValidationErrorResponse.cs -
Modify:
run-dev.sh -
Create:
tests/BuildFlow.Host.Contract.Tests/AuthContractTests.cs -
Step 1: Write failing auth and Swagger contract tests
Add tests that assert:
[Fact] public async Task Swagger_is_anonymous_and_contains_buildflow_paths();
[Fact] public async Task Empty_login_returns_400_invalid_param();
[Fact] public async Task Bad_credentials_return_401_unauthorized();
[Fact] public async Task Login_returns_access_token_and_http_only_refresh_cookie();
[Fact] public async Task Current_user_menu_and_codes_require_and_accept_access_token();
[Fact] public async Task Refresh_rotates_cookie_and_old_cookie_is_rejected();
[Fact] public async Task Logout_revokes_refresh_cookie();
Deserialize errors with:
private sealed record ErrorEnvelope(string Code, string Message, JsonElement? Data);
private sealed record SuccessEnvelope<T>(int Code, string Message, T Data);
For Swagger, assert /api/auth/login, /api/user/info, /api/menu/all, and /api/auth/codes exist in paths.
- Step 2: Run the tests to prove the current contract is wrong
Run:
dotnet test tests/BuildFlow.Host.Contract.Tests --filter AuthContractTests
Expected: Swagger is 401; validation uses numeric code 400; bad credentials map to HTTP 400 instead of 401.
- Step 3: Implement one semantic-code mapper
Create this public shape in ApiErrorMapper.cs:
public readonly record struct ApiErrorDescriptor(
HttpStatusCode StatusCode,
string Code,
string Message);
public static class ApiErrorMapper
{
public static ApiErrorDescriptor Map(Exception exception) => exception switch
{
UnauthorizedException e => Describe(HttpStatusCode.Unauthorized, e.Code, e.Message),
NotFoundException e => Describe(HttpStatusCode.NotFound, e.Code, e.Message),
BusinessException e => Describe(StatusForBusinessCode(e.Code), e.Code, e.Message),
ValidationException e => Describe(
HttpStatusCode.BadRequest,
ErrorCode.InvalidParam,
string.Join("; ", e.Errors.Select(x => x.ErrorMessage))),
_ => Describe(HttpStatusCode.InternalServerError, ErrorCode.Internal, "服务内部错误,请稍后重试")
};
private static HttpStatusCode StatusForBusinessCode(string code) => code switch
{
ErrorCode.Unauthorized => HttpStatusCode.Unauthorized,
ErrorCode.Forbidden => HttpStatusCode.Forbidden,
ErrorCode.NotFound => HttpStatusCode.NotFound,
ErrorCode.AlreadyExists or ErrorCode.Conflict => HttpStatusCode.Conflict,
ErrorCode.RateLimited => HttpStatusCode.TooManyRequests,
ErrorCode.TooLarge => HttpStatusCode.RequestEntityTooLarge,
ErrorCode.Expired or ErrorCode.QuotaExceeded => HttpStatusCode.Gone,
_ => HttpStatusCode.BadRequest
};
private static ApiErrorDescriptor Describe(
HttpStatusCode statusCode,
string code,
string message) => new(statusCode, code, message);
}
Use it from both shared exception handlers. In AggregateExceptionHandler, select the descriptor like this before writing the response:
var descriptor = ex switch
{
UnauthorizedException or NotFoundException or BusinessException or ValidationException
=> ApiErrorMapper.Map(ex),
SharePasswordException
=> new ApiErrorDescriptor(HttpStatusCode.Unauthorized, ErrorCode.Unauthorized, ex.Message),
ForbiddenException
=> new ApiErrorDescriptor(HttpStatusCode.Forbidden, ErrorCode.Forbidden, ex.Message),
ConflictException
=> new ApiErrorDescriptor(HttpStatusCode.Conflict, ErrorCode.Conflict, ex.Message),
ShareExpiredException
=> new ApiErrorDescriptor(HttpStatusCode.Gone, ErrorCode.Expired, ex.Message),
ShareExhaustedException
=> new ApiErrorDescriptor(HttpStatusCode.Gone, ErrorCode.QuotaExceeded, ex.Message),
TooLargeException
=> new ApiErrorDescriptor(HttpStatusCode.RequestEntityTooLarge, ErrorCode.TooLarge, ex.Message),
_ => ApiErrorMapper.Map(ex),
};
- Step 4: Return the semantic validation envelope
Change the FastEndpoints validation builder to:
config.Errors.ResponseBuilder = (failures, _, _) =>
new ErrorResponse(
ErrorCode.InvalidParam,
string.Join("; ", failures.Select(x => x.ErrorMessage)));
Delete ValidationErrorResponse.cs; it must have no remaining references.
- Step 5: Serve Swagger before authentication
Move the existing app.UseSwaggerGen(...) block so the effective order is:
CORS -> Routing -> GlobalException -> Swagger -> gRPC -> ApiResponse
-> Authentication -> PermissionVersion -> Authorization -> FastEndpoints
Do not add AllowAnonymous() to business endpoints and do not move UseAuthorization() after FastEndpoints.
- Step 6: Remove the contradictory 5200 startup argument
Change the final run-dev.sh line to:
exec dotnet run
Run chmod +x run-dev.sh so the documented ./run-dev.sh invocation works.
The Host then uses the single local port declared by Kestrel: 5667.
- Step 7: Run contract, architecture, and platform tests
Run:
dotnet test tests/BuildFlow.Host.Contract.Tests
dotnet test tests/BuildFlow.Architecture.Tests --no-restore
dotnet test tests/BuildFlow.Platform.Tests --no-restore
dotnet build BuildFlow.slnx --no-restore -m:1
Expected: all commands pass; Swagger is anonymous while protected APIs remain protected.
- Step 8: Commit the backend contract
git add run-dev.sh src/BuildFlow.Host/BuildFlowHostModule.cs src/BuildFlow.Host/AggregateExceptionHandler.cs src/shared/BuildFlow.Shared/Api tests/BuildFlow.Host.Contract.Tests/AuthContractTests.cs
git commit -m "fix: standardize host api contracts"
Task 5: Add Deterministic OpenAPI Snapshot and Generation
Files:
-
Modify:
/Users/wen/project/rag/build-flow.admin/config/config.ts -
Modify:
/Users/wen/project/rag/build-flow.admin/config/proxy.ts -
Move:
/Users/wen/project/rag/build-flow.admin/config/oneapi.json->/Users/wen/project/rag/build-flow.admin/config/openapi/ant-design-pro.json -
Create:
/Users/wen/project/rag/build-flow.admin/config/openapi/buildflow.json -
Create:
/Users/wen/project/rag/build-flow.admin/scripts/openapi-utils.mjs -
Create:
/Users/wen/project/rag/build-flow.admin/scripts/sync-openapi.mjs -
Create:
/Users/wen/project/rag/build-flow.admin/scripts/generate-openapi.mjs -
Create:
/Users/wen/project/rag/build-flow.admin/scripts/generate-template-openapi.mjs -
Create:
/Users/wen/project/rag/build-flow.admin/scripts/check-openapi.mjs -
Create:
/Users/wen/project/rag/build-flow.admin/tests/openapi/openapi-utils.test.mjs -
Modify:
/Users/wen/project/rag/build-flow.admin/package.json -
Modify:
/Users/wen/project/rag/build-flow.admin/src/app.tsx -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/transport/types.ts -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/transport/request.ts -
Step 1: Write failing OpenAPI validation tests
Test these exact rules with Node's built-in test runner:
assert.throws(() => validateOpenApiDocument({}), /OpenAPI 3/);
assert.throws(
() => validateOpenApiDocument({ openapi: '3.0.1', paths: { '/api/rule': {} }, components: { schemas: {} } }),
/missing required path/,
);
assert.equal(stableJson({ b: 1, a: { d: 2, c: 1 } }), '{\n "a": {\n "c": 1,\n "d": 2\n },\n "b": 1\n}\n');
Run:
node --test tests/openapi/openapi-utils.test.mjs
Expected: FAIL because scripts/openapi-utils.mjs does not exist.
- Step 2: Implement stable validation helpers
Export:
export const REQUIRED_PATHS = [
'/api/auth/login',
'/api/user/info',
'/api/menu/all',
'/api/auth/codes',
'/api/users',
'/api/knowledge-bases',
'/api/workflow-definitions',
'/api/scripts',
'/api/monitoring/systems',
];
export function validateOpenApiDocument(document) {
if (!document?.openapi?.startsWith('3.')) throw new Error('OpenAPI 3 document required');
if (!document.paths || !document.components?.schemas) throw new Error('paths and schemas are required');
for (const path of REQUIRED_PATHS) {
if (!document.paths[path]) throw new Error(`missing required path: ${path}`);
}
if (document.paths['/api/rule'] || document.paths['/api/currentUser']) {
throw new Error('Ant Design Pro demo schema is not a BuildFlow contract');
}
if (Object.keys(document.paths).length < 170) throw new Error('unexpectedly small BuildFlow contract');
return document;
}
Implement recursive key sorting and stableJson() with a final newline.
- Step 3: Implement sync and generation scripts
sync-openapi.mjs must:
const origin = process.env.BUILDFLOW_API_ORIGIN ?? 'http://127.0.0.1:5667';
const response = await fetch(new URL('/swagger/v1/swagger.json', origin));
if (!response.ok) throw new Error(`Swagger request failed: ${response.status}`);
const document = validateOpenApiDocument(await response.json());
await writeFile(tempPath, stableJson(document), 'utf8');
await rename(tempPath, snapshotPath);
generate-openapi.mjs must call generateService() with:
Before writing the generators, preserve the original template contract:
mkdir -p config/openapi
git mv config/oneapi.json config/openapi/ant-design-pro.json
{
schemaPath: snapshotPath,
serversPath: path.join(root, 'src/services'),
projectName: 'buildflow/generated',
namespace: 'BuildFlowAPI',
requestLibPath: "import { request } from '@/services/buildflow/transport/request'\nimport type { BuildFlowRequestOptions } from '@/services/buildflow/transport/types'",
requestOptionsType: 'BuildFlowRequestOptions',
splitDeclare: true,
isCamelCase: true,
nullable: false
}
Create the generator-facing options type before generation:
import type { RequestOptions } from '@umijs/max';
export interface BuildFlowRequestOptions extends RequestOptions {
responseKind?: 'json' | 'text' | 'blob' | 'raw';
silent?: boolean;
skipAuthRefresh?: boolean;
__buildFlowRetried?: boolean;
}
The generator-facing request file is:
import { request as umiRequest } from '@umijs/max';
import type { RequestOptions } from '@umijs/max';
export function request<T>(url: string, options?: RequestOptions): Promise<T> {
return umiRequest<T>(url, options);
}
- Step 4: Implement generated-output drift checking
check-openapi.mjs must generate into mkdtemp() beneath the OS temp directory, recursively compare relative file names and UTF-8 contents with src/services/buildflow/generated/, then delete the temporary directory in finally.
Exit non-zero with one of these concrete messages:
generated file missing: <path>
generated file is stale: <path>
unexpected generated file: <path>
generate-template-openapi.mjs must use config/openapi/ant-design-pro.json, projectName: 'ant-design-pro', namespace: 'API', and requestLibPath: "import { request } from '@umijs/max'". It is the only command allowed to rewrite src/services/ant-design-pro/.
- Step 5: Remove hidden Umi OpenAPI generation and point development Swagger at the backend
In config/config.ts:
- remove
@umijs/max-plugin-openapifromplugins; - remove the
openAPIconfiguration block; - keep
@umijs/request-recordonly if existing tests still use it.
Then remove the no-longer-used plugin dependency:
npm uninstall --save-dev @umijs/max-plugin-openapi
In config/proxy.ts, proxy both /api/ and /swagger/ to BUILDFLOW_API_TARGET, defaulting to http://127.0.0.1:5667.
Change the development OpenAPI link in src/app.tsx from /umi/plugin/openapi to /swagger/index.html.
- Step 6: Add package scripts
Add:
"api:sync": "node scripts/sync-openapi.mjs",
"api:generate": "node scripts/generate-openapi.mjs",
"api:check": "node scripts/check-openapi.mjs",
"api:generate:template": "node scripts/generate-template-openapi.mjs",
"openapi": "npm run api:generate",
"test:contracts": "node --test tests/openapi/*.test.mjs"
- Step 7: Start the Testing Host and generate the real contract
Run in terminal A:
cd /Users/wen/project/rag/build-flow.api
ASPNETCORE_ENVIRONMENT=Testing dotnet run --project src/BuildFlow.Host
Run in terminal B:
cd /Users/wen/project/rag/build-flow.admin
npm run test:contracts
npm run api:sync
npm run api:generate
npm run api:check
Expected: the snapshot has at least 170 paths, generated TypeScript compiles, and api:check reports no drift.
- Step 8: Commit the contract tooling
git add config package.json package-lock.json src/app.tsx scripts/openapi-utils.mjs scripts/sync-openapi.mjs scripts/generate-openapi.mjs scripts/generate-template-openapi.mjs scripts/check-openapi.mjs tests/openapi src/services/buildflow
git commit -m "feat: add deterministic buildflow api generation"
Task 6: Implement the Typed Request, Error, Token, and Refresh Core
Files:
-
Modify:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/transport/types.ts -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/transport/error.ts -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/transport/tokenStore.ts -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/transport/refresh.ts -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/transport/session.ts -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/transport/requestConfig.ts -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/transport/RequestNotifier.tsx -
Modify:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/transport/request.ts -
Modify:
/Users/wen/project/rag/build-flow.admin/src/requestErrorConfig.ts -
Modify:
/Users/wen/project/rag/build-flow.admin/src/app.tsx -
Test:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/transport/*.test.ts -
Step 1: Write failing envelope and token-store tests
Cover these exact outcomes:
expect(unwrapEnvelope({ code: 0, message: 'ok', data: { id: 1 } })).toEqual({ id: 1 });
expect(() => unwrapEnvelope({ code: 'invalid_param', message: 'bad', data: null }))
.toThrow(BuildFlowRequestError);
setAccessToken('abc');
expect(getAccessToken()).toBe('abc');
clearAccessToken();
expect(getAccessToken()).toBeNull();
Run:
npm test -- src/services/buildflow/transport/error.test.ts src/services/buildflow/transport/tokenStore.test.ts --runInBand
Expected: FAIL because the transport modules do not exist.
- Step 2: Define stable transport types and normalized errors
Use these public shapes:
export interface BuildFlowEnvelope<T> {
code: number | string;
message: string;
data: T;
}
export class BuildFlowRequestError extends Error {
constructor(
message: string,
readonly code: string = 'request_error',
readonly status?: number,
readonly data?: unknown,
options?: ErrorOptions,
) {
super(message, options);
this.name = 'BuildFlowRequestError';
}
}
unwrapEnvelope() accepts only code 0 or '0' as success.
- Step 3: Write the failing concurrent-refresh test
Use a deferred fetch mock and call refreshAccessToken() 20 times before resolving it. Assert:
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(await Promise.all(promises)).toEqual(Array(20).fill('new-token'));
Also assert empty text, non-2xx, and JSON error responses reject with BuildFlowRequestError.
- Step 4: Implement refresh de-duplication with native fetch
The runtime contract is:
let inFlightRefresh: Promise<string> | undefined;
export function refreshAccessToken(fetchImpl: typeof fetch = fetch) {
inFlightRefresh ??= performRefresh(fetchImpl).finally(() => {
inFlightRefresh = undefined;
});
return inFlightRefresh;
}
performRefresh() POSTs /api/auth/refresh with credentials: 'include', reads non-empty text, writes the access token, and never reads or stores the refresh cookie.
- Step 5: Write failing retry and single-logout tests
Create the rejected-interceptor through dependency injection and assert:
-
a 401 with an access token calls refresh and retry once;
-
20 simultaneous 401s share one refresh call;
-
login, refresh, anonymous share, and
__buildFlowRetriedrequests never refresh; -
refresh failure clears the token and calls the redirect callback once.
-
Step 6: Implement the Umi request configuration
Export:
export const buildFlowRequestConfig: RequestConfig = {
baseURL: '/api',
withCredentials: true,
requestInterceptors: [attachBuildFlowHeaders],
responseInterceptors: [[unwrapBuildFlowResponse, handleBuildFlowResponseError]],
errorConfig: { errorHandler: showBuildFlowError },
};
Rules:
- JSON envelopes are unwrapped by replacing
response.datawithenvelope.data. - text, Blob, ArrayBuffer, and
responseKind: 'raw'bypass envelope parsing. - retry copies the original Axios config, adds
__buildFlowRetried: true, updates Authorization, and preserves cancellation/progress callbacks. - auth failure uses
history.replace('/user/login?redirect=...')once. - the transport does not call static
message.error; expose an error-notification subscriber thatApp.useApp()registers from a mounted component.
Implement and mount this component from layout.childrenRender:
export function RequestNotifier() {
const { message } = App.useApp();
useEffect(() => subscribeToRequestErrors((text) => message.error(text)), [message]);
return null;
}
Keep src/requestErrorConfig.ts as a compatibility re-export only:
export { buildFlowRequestConfig as errorConfig } from '@/services/buildflow/transport/requestConfig';
export { clearAccessToken, getAccessToken, setAccessToken } from '@/services/buildflow/transport/tokenStore';
- Step 7: Make generated requests use the BuildFlow options type
Update transport/request.ts:
export function request<T>(
url: string,
options?: BuildFlowRequestOptions,
): Promise<T> {
return umiRequest<T>(url, options);
}
Regenerate and run npm run api:check.
- Step 8: Verify and commit the request core
Run:
npm test -- src/services/buildflow/transport --runInBand
npm run api:check
npm run lint
npx antd lint ./src
npm run build
Expected: all commands exit 0 and antd lint reports no static feedback API usage in the BuildFlow transport.
Commit:
git add src/app.tsx src/requestErrorConfig.ts src/services/buildflow/transport src/services/buildflow/generated
git commit -m "feat: add resilient buildflow request transport"
Task 7: Add Download, Upload, SSE, and SignalR Boundaries
Files:
-
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/download.ts -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/upload.ts -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/sse.ts -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/signalr.ts -
Test:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/*.test.ts -
Step 1: Write failing pure tests for protocol boundaries
Assert:
expect(parseContentDisposition("attachment; filename*=UTF-8''report%20one.pdf"))
.toBe('report one.pdf');
expect(parseSseFrames('event: token\ndata: hello\n\ndata: world\n\n'))
.toEqual([
{ event: 'token', data: 'hello', id: undefined },
{ event: 'message', data: 'world', id: undefined },
]);
expect(createSignalRAccessTokenFactory()()).toBe('stored-token');
For upload, mock request() and assert the options contain FormData but no explicit Content-Type header.
- Step 2: Implement download and upload helpers
Public APIs:
export async function downloadFile(
url: string,
options?: BuildFlowRequestOptions,
): Promise<{ blob: Blob; fileName?: string }>;
export async function uploadFormData<T>(
url: string,
formData: FormData,
options?: BuildFlowRequestOptions,
): Promise<T>;
Download requests use responseType: 'blob', getResponse: true, and responseKind: 'blob'. Upload requests never set the multipart boundary.
- Step 3: Implement cancellable SSE parsing
Public APIs:
export interface SseEvent {
event: string;
data: string;
id?: string;
}
export async function* streamSse(
url: string,
init: RequestInit = {},
): AsyncGenerator<SseEvent>;
The implementation uses native fetch, adds the current bearer token, retries one initial 401 through refreshAccessToken(), parses \n\n and \r\n\r\n frame boundaries, flushes the final buffered frame, and honors the caller's AbortSignal.
- Step 4: Implement the SignalR connection factory
Use:
export function createHubConnection(path: string): HubConnection {
return new HubConnectionBuilder()
.withUrl(path, { accessTokenFactory: createSignalRAccessTokenFactory() })
.withAutomaticReconnect([0, 2_000, 5_000, 10_000])
.configureLogging(LogLevel.Warning)
.build();
}
Do not start the connection inside the factory; pages own lifecycle and subscriptions.
- Step 5: Verify and commit special transports
npm test -- src/services/buildflow/download.test.ts src/services/buildflow/upload.test.ts src/services/buildflow/sse.test.ts src/services/buildflow/signalr.test.ts --runInBand
npm run lint
npm run build
git add src/services/buildflow package.json package-lock.json
git commit -m "feat: add buildflow protocol adapters"
Task 8: Connect Auth, Initial State, Menus, Access Codes, Login, and Logout
Files:
-
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/contracts.ts -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/adapters/auth.ts -
Create:
/Users/wen/project/rag/build-flow.admin/src/services/buildflow/adapters/auth.test.ts -
Modify:
/Users/wen/project/rag/build-flow.admin/src/app.tsx -
Modify:
/Users/wen/project/rag/build-flow.admin/src/access.ts -
Modify:
/Users/wen/project/rag/build-flow.admin/config/routes.ts -
Modify:
/Users/wen/project/rag/build-flow.admin/src/utils/menuTransform.tsx -
Create:
/Users/wen/project/rag/build-flow.admin/src/utils/menuTransform.test.tsx -
Create:
/Users/wen/project/rag/build-flow.admin/src/access.test.ts -
Modify:
/Users/wen/project/rag/build-flow.admin/src/pages/user/login/index.tsx -
Modify:
/Users/wen/project/rag/build-flow.admin/src/pages/user/login/login.test.tsx -
Delete:
/Users/wen/project/rag/build-flow.admin/src/pages/user/login/__snapshots__/login.test.tsx.snap -
Modify:
/Users/wen/project/rag/build-flow.admin/src/components/RightContent/AvatarDropdown.tsx -
Step 1: Define stable UI contracts
Create these stable types rather than exposing generated names to pages:
export interface LoginCredentials { username: string; password: string }
export interface TokenResponse { accessToken: string; expiresAt: number | string }
export interface CurrentUser {
id: string;
username: string;
name: string;
realName: string;
avatar?: string;
description?: string;
homePath: string;
roles: string[];
email?: string;
}
export interface MenuMeta {
title: string;
icon?: string;
order?: number;
link?: string;
hideInMenu?: boolean;
hideChildrenInMenu?: boolean;
hideInBreadcrumb?: boolean;
noBasicLayout?: boolean;
authority?: string[];
renderType?: string;
formDefinitionId?: string;
runtimeViewMode?: string;
}
export interface MenuRoute {
name: string;
path: string;
component?: string;
redirect?: string;
meta: MenuMeta;
children: MenuRoute[];
}
export interface AuthenticatedState {
currentUser: CurrentUser;
menuRoutes: MenuRoute[];
accessCodes: string[];
}
- Step 2: Write failing adapter mapping tests
Mock transport responses and assert:
-
login stores only
accessToken; -
backend
userId,realName,descmap toid,name,description; -
missing arrays become empty arrays;
-
loadAuthenticatedState()requests current user, menu, and access codes concurrently; -
logout calls
/auth/logoutwith credentials and clears the token infinally. -
Step 3: Implement the auth adapter
Public API:
export async function login(credentials: LoginCredentials): Promise<TokenResponse>;
export async function logout(): Promise<void>;
export async function getCurrentUser(): Promise<CurrentUser>;
export async function getCurrentUserMenus(): Promise<MenuRoute[]>;
export async function getAccessCodes(): Promise<string[]>;
export async function loadAuthenticatedState(): Promise<AuthenticatedState>;
Use direct stable paths (/auth/login, /auth/logout, /user/info, /menu/all, /auth/codes) through the BuildFlow request wrapper. These cross-cutting auth paths remain handwritten even though the general API client is generated.
- Step 4: Write failing access and menu tests
Assert:
expect(access({ accessCodes: ['user:read'], currentUser }).can('user:read')).toBe(true);
expect(access({ accessCodes: ['user:read'], currentUser }).canAll(['user:read', 'user:update'])).toBe(false);
const routes: MenuRoute[] = [{
name: 'system',
path: '/system',
meta: { title: '系统管理' },
children: [{
name: 'user',
path: 'user',
meta: { title: '用户管理' },
children: [],
}],
}];
expect(transformMenuRoutes(routes)[0].routes?.[0].path)
.toBe('/system/user');
Include external links, hidden menu fields, and icons in menu tests.
- Step 5: Implement initial state and access helpers
getInitialState() returns:
{
currentUser?: CurrentUser;
menuRoutes: MenuRoute[];
accessCodes: string[];
fetchAuthenticatedState: () => Promise<AuthenticatedState | undefined>;
settings: Partial<LayoutSettings>;
settingDrawerOpen: boolean;
}
When an access token exists and the route is not an auth page, call loadAuthenticatedState() once. On failure, let the transport refresh once; only then redirect.
Add this compile-time route so the seeded backend home path cannot land on 404 before the dashboard phase is implemented:
{
path: '/dashboard/workspace',
component: './Welcome',
}
Do not add names or icons; the backend menu remains the display source. Other business paths stay unimplemented until their own stages.
access.ts returns:
{
can: (code: string) => codeSet.has(code),
canAny: (codes: string[]) => codes.some((code) => codeSet.has(code)),
canAll: (codes: string[]) => codes.every((code) => codeSet.has(code)),
canAdmin: roles.some((role) => ['admin', 'superadmin'].includes(role.toLowerCase())),
canLogin: Boolean(currentUser),
}
- Step 6: Replace the demo login test before changing the page
Remove request-record server and snapshot assertions. Mock the auth adapter and assert the page:
- shows BuildFlow, username, and password fields;
- rejects empty fields client-side;
- passes exact credentials to
login(); - shows the backend error message on rejection;
- preserves only same-origin relative redirect paths.
Run the test and expect it to fail against the current demo page.
- Step 7: Simplify the login page to the real backend capability
Keep LoginForm, ProFormText, and ProFormText.Password. Remove mobile tabs, captcha, social icons, fake credentials, and nonfunctional forgot-password link.
Use:
<LoginForm
logo={<img alt="BuildFlow" src="/logo.svg" />}
title="BuildFlow"
subTitle="统一业务流程与知识工作台"
onFinish={handleSubmit}
>
On success, load authenticated state into @@initialState and navigate to the validated redirect or currentUser.homePath. On error, show the normalized backend message through App.useApp().
- Step 8: Connect real logout
AvatarDropdown calls logout(), clears currentUser, menuRoutes, and accessCodes, then redirects with the current location. The finally cleanup must run even if the network logout call fails.
- Step 9: Restore the Ant Design Pro generated directory to non-BuildFlow ownership
After all BuildFlow imports point to src/services/buildflow/, run the dedicated template generator. It deletes stale custom files and restores the directory from config/openapi/ant-design-pro.json without manual edits:
npm run api:generate:template
Confirm:
rg -n "auth/login|user/info|menu/all|buildflow_access_token" src/services/ant-design-pro
Expected: no matches.
- Step 10: Run frontend integration tests and commit
npm test -- src/services/buildflow/adapters/auth.test.ts src/utils/menuTransform.test.tsx src/access.test.ts src/pages/user/login/login.test.tsx --runInBand
npm run lint
npx antd lint ./src
npm run build
git add config/routes.ts src/app.tsx src/access.ts src/utils src/pages/user/login src/components/RightContent/AvatarDropdown.tsx src/services
git commit -m "feat: connect buildflow authentication state"
Task 9: Add a Credential-Safe Real API Smoke Suite
Files:
-
Create:
/Users/wen/project/rag/build-flow.admin/scripts/api-smoke.mjs -
Create:
/Users/wen/project/rag/build-flow.admin/tests/smoke/api-smoke.test.mjs -
Modify:
/Users/wen/project/rag/build-flow.admin/package.json -
Step 1: Extract and test cookie/token-safe smoke helpers
Write pure tests for:
extractCookie(setCookieHeaders, 'jwt');
assertEnvelope(responseJson, 0);
countMenuNodes(menuTree);
redactError(new Error('Bearer secret-value'));
redactError() must replace bearer tokens and jwt= values before printing.
- Step 2: Implement the real smoke sequence
Use BUILDFLOW_API_ORIGIN (default http://127.0.0.1:5667), BUILDFLOW_TEST_USERNAME (default admin), and BUILDFLOW_TEST_PASSWORD (default admin123). Never print their values.
Verify in order:
- anonymous Swagger is 200 and contains required paths;
- empty login is 400
invalid_param; - bad credentials are 401
unauthorized; - successful login is code 0, returns access token, and sets HttpOnly
jwt; - current user, menu, and access codes succeed with bearer token;
- menu node count is at least 30 and access-code count is at least 83;
- refresh returns non-empty text and rotates the cookie;
- the old refresh cookie is rejected;
- logout clears/revokes the rotated cookie;
- anonymous protected requests return 401.
Print only one line per check, for example PASS auth.login.success.
- Step 3: Add and run the smoke command
Add:
"test:smoke": "node scripts/api-smoke.mjs"
Run with the Testing Host active:
npm run test:smoke
Expected: all checks print PASS; output contains no JWT-looking string and no cookie value.
- Step 4: Commit the smoke suite
git add package.json package-lock.json scripts/api-smoke.mjs tests/smoke
git commit -m "test: add buildflow api smoke coverage"
Task 10: Add Playwright Authentication and Refresh E2E
Files:
-
Create:
/Users/wen/project/rag/build-flow.admin/playwright.config.ts -
Create:
/Users/wen/project/rag/build-flow.admin/tests/e2e/auth.spec.ts -
Modify:
/Users/wen/project/rag/build-flow.admin/package.json -
Step 1: Add Playwright configuration
Use:
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: false,
retries: 0,
use: {
baseURL: 'http://127.0.0.1:8000',
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
},
webServer: {
command: 'npm run dev',
url: 'http://127.0.0.1:8000/user/login',
reuseExistingServer: false,
env: { BUILDFLOW_API_TARGET: 'http://127.0.0.1:5667' },
timeout: 120_000,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile-chromium', use: { ...devices['Pixel 7'] } },
],
});
- Step 2: Write the real browser scenarios
Use seeded Testing credentials from environment variables and cover:
test('rejects bad credentials without leaving login');
test('logs in and renders the backend user and menu');
test('keeps the session after a page reload');
test('refreshes once after one injected 401 and replays the request');
test('redirects with the current URL when refresh fails');
test('logs out and cannot refresh the revoked session');
For the refresh-success test, intercept the first /api/user/info response with a 401 envelope, continue subsequent attempts, count /api/auth/refresh requests, and assert the count is exactly one.
For refresh failure, clear browser cookies while preserving local storage, inject a 401, and assert the final URL is /user/login?redirect=....
- Step 3: Install the browser and run E2E
npx playwright install chromium
npm run test:e2e
Add:
"test:e2e": "playwright test"
Expected: all six scenarios pass against the real Testing Host on desktop and mobile Chromium. Capture successful login/menu screenshots with page.screenshot() into each test project's output directory and inspect them for clipping or overlap.
- Step 4: Commit E2E coverage
git add package.json package-lock.json playwright.config.ts tests/e2e
git commit -m "test: cover buildflow auth in playwright"
Task 11: Publish the Closed-Loop SOP and Run the Stage Gate
Files:
-
Modify:
/Users/wen/project/rag/build-flow.admin/package.json -
Modify:
/Users/wen/project/rag/build-flow.admin/docs/BACKEND_INTEGRATION.md -
Create:
/Users/wen/project/rag/build-flow.admin/docs/INTEGRATION_SOP.md -
Create:
/Users/wen/project/rag/build-flow.api/scripts/testing-host.sh -
Modify:
/Users/wen/project/rag/build-flow.api/docs/superpowers/specs/2026-07-20-react-pro-integration-foundation-design.mdonly if implementation evidence changes an approved factual statement -
Step 1: Add one backend Testing Host lifecycle script
Create scripts/testing-host.sh with start, stop, and status commands. start must:
- require
scripts/test-infra.sh upto have succeeded; - start
ASPNETCORE_ENVIRONMENT=Testing dotnet run --project src/BuildFlow.Host --no-build; - store only the PID under
${TMPDIR:-/tmp}/buildflow-testing-host.pid; - wait up to 120 seconds for
http://127.0.0.1:5667/health; - print the log path, never configuration values.
stop sends TERM, waits, and removes the PID file.
Run chmod +x scripts/testing-host.sh before committing it.
- Step 2: Add the frontend offline verification command
Add:
"antd:lint": "antd lint ./src",
"test:unit": "jest --runInBand",
"verify": "npm run api:check && npm run lint && npm run test:contracts && npm run test:unit && npm run antd:lint && npm run build"
verify must not start containers or contact a running backend.
- Step 3: Rewrite the integration guide around the actual contract
docs/BACKEND_INTEGRATION.md must state:
- npm and
package-lock.jsononly; - backend port 5667 only;
npm run api:syncrequires a running backend, while build andapi:checkdo not;- response success code is 0;
- refresh is plain text and Cookie-based;
- generated and handwritten ownership boundaries;
- the eight-stage program and the current completed stage.
Remove the stale Phase 1 claim that refresh is missing after tests prove it works.
- Step 4: Write the operator SOP
docs/INTEGRATION_SOP.md must provide this exact lifecycle:
# backend
cd /Users/wen/project/rag/build-flow.api
bash scripts/test-infra.sh up
dotnet build BuildFlow.slnx --no-restore -m:1
dotnet test tests/BuildFlow.Host.Contract.Tests --no-build
bash scripts/testing-host.sh start
# contract sync and frontend offline gate
cd /Users/wen/project/rag/build-flow.admin
npm ci
npm run api:sync
npm run api:generate
npm run verify
# real integration
npm run test:smoke
npm run test:e2e
# cleanup
cd /Users/wen/project/rag/build-flow.api
bash scripts/testing-host.sh stop
bash scripts/test-infra.sh down
Include failure triage by layer: install, contract, backend contract tests, request unit tests, API smoke, browser E2E.
- Step 5: Run the complete first-stage backend gate
With test infrastructure running:
dotnet build BuildFlow.slnx --no-restore -m:1
dotnet test tests/BuildFlow.Architecture.Tests --no-build
dotnet test tests/BuildFlow.Platform.Tests --no-build
dotnet test tests/BuildFlow.Host.Contract.Tests --no-build
dotnet test BuildFlow.slnx --no-build -m:1
Expected:
-
build, Architecture, Platform, and Host Contract suites are green;
-
the full-suite result is recorded with exact pass/fail counts;
-
infrastructure connection failures are not accepted as skips or ignored failures;
-
any remaining RAG/Scripting failures are filed into the next active defect task and must be zero before the overall integration goal can be completed.
-
Step 6: Run the complete frontend gate from a clean install
rm -rf node_modules
npm ci
npm run verify
npm run test:smoke
npm run test:e2e
Expected: every command exits 0.
- Step 7: Verify generated ownership and secret-safe logs
Run:
git diff --exit-code -- src/services/buildflow/generated config/openapi/buildflow.json
rg -n "auth/login|user/info|menu/all|buildflow_access_token" src/services/ant-design-pro
Expected: generated files are clean; the second command has no matches. Inspect smoke/E2E output and confirm it contains no bearer token, Cookie value, connection string, or password.
- Step 8: Commit SOP files in their respective repositories
Frontend:
git add package.json package-lock.json docs/BACKEND_INTEGRATION.md docs/INTEGRATION_SOP.md
git commit -m "docs: add buildflow integration sop"
Backend:
git add scripts/testing-host.sh
git commit -m "test: add testing host lifecycle"
- Step 9: Update the program plan for Stage 2
Do not mark the overall goal complete. Record first-stage evidence and start a new brainstorming/spec cycle for Platform pages: users, roles, permissions, menus, storage, profile, and notifications.
Spec Traceability
- Engineering/npm baseline: Tasks 1-2.
- Anonymous Swagger, semantic validation, and auth contract: Tasks 3-4.
- Reproducible OpenAPI snapshot and immutable generated client: Task 5.
- Token, envelope, concurrent refresh, replay, and normalized errors: Task 6.
- Download, upload, SSE, and SignalR ownership boundaries: Task 7.
- Login, logout, current user, 30-node menu, 83 access codes, and valid home path: Task 8.
- Real HTTP smoke coverage: Task 9.
- Browser login, reload, refresh success/failure, logout, desktop, and mobile coverage: Task 10.
- Repeatable clean-install SOP and full stage verification: Task 11.
Stage 1 Completion Evidence
The implementation worker must preserve the following evidence before claiming Stage 1 complete:
- frontend commit hashes for npm baseline, OpenAPI generation, request transport, auth integration, smoke, E2E, and SOP;
- backend commit hashes for test environment, contract fixes, and Testing Host lifecycle;
- anonymous Swagger HTTP 200 and normalized snapshot path/schema counts;
- frontend
verify, smoke, and Playwright output; - backend build and test counts, including any remaining failures outside the Stage 1 acceptance boundary;
- screenshots for successful login, loaded dynamic menu, and refresh-failure redirect at desktop and mobile viewport widths;
- a clean generated-code drift check.