diff --git a/docs/superpowers/plans/2026-07-20-react-pro-integration-foundation.md b/docs/superpowers/plans/2026-07-20-react-pro-integration-foundation.md new file mode 100644 index 0000000..3507d5b --- /dev/null +++ b/docs/superpowers/plans/2026-07-20-react-pro-integration-foundation.md @@ -0,0 +1,1704 @@ +# 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 only `src/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-facing `request()` 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 and `WebApplicationFactory` entry 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: + +```bash +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: + +```bash +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: + +```bash +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: + +```json +"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`: + +```json +"!cloudflare-worker/**", +"!src/services/ant-design-pro/**", +"!src/services/buildflow/generated/**" +``` + +Change the rules preset from: + +```json +"recommended": true +``` + +to: + +```json +"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`: + +```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: + +```bash +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** + +```bash +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: + +```bash +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: + +```ts +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: + +```bash +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** + +```bash +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: + +```yaml +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: + +```sql +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`: + +```bash +#!/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 " >&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: + +```json +{ + "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: + +```csharp +if (!builder.Environment.IsEnvironment("Testing")) +{ + builder.Configuration.AddJsonFile( + "appsettings.Secrets.json", + optional: true, + reloadOnChange: true); +} +builder.Configuration.AddEnvironmentVariables(); +``` + +Append: + +```csharp +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: + +```csharp +public sealed class BuildFlowHostFactory : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment("Testing"); + } +} +``` + +Use a shared collection fixture and add this test: + +```csharp +[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 +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: + +```csharp +services.AddHealthChecks(); +``` + +During application initialization, map: + +```csharp +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: + +```csharp +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: + +```bash +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: + +```bash +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: + +```csharp +[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: + +```csharp +private sealed record ErrorEnvelope(string Code, string Message, JsonElement? Data); +private sealed record SuccessEnvelope(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: + +```bash +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`: + +```csharp +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: + +```csharp +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: + +```csharp +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: + +```text +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: + +```bash +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: + +```bash +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** + +```bash +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: + +```js +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: + +```bash +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: + +```js +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: + +```js +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: + +```bash +mkdir -p config/openapi +git mv config/oneapi.json config/openapi/ant-design-pro.json +``` + +```js +{ + 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: + +```ts +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: + +```ts +import { request as umiRequest } from '@umijs/max'; +import type { RequestOptions } from '@umijs/max'; + +export function request(url: string, options?: RequestOptions): Promise { + return umiRequest(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: + +```text +generated file missing: +generated file is stale: +unexpected generated file: +``` + +`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-openapi` from `plugins`; +- remove the `openAPI` configuration block; +- keep `@umijs/request-record` only if existing tests still use it. + +Then remove the no-longer-used plugin dependency: + +```bash +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: + +```json +"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: + +```bash +cd /Users/wen/project/rag/build-flow.api +ASPNETCORE_ENVIRONMENT=Testing dotnet run --project src/BuildFlow.Host +``` + +Run in terminal B: + +```bash +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** + +```bash +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: + +```ts +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: + +```bash +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: + +```ts +export interface BuildFlowEnvelope { + 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: + +```ts +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: + +```ts +let inFlightRefresh: Promise | 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 `__buildFlowRetried` requests never refresh; +- refresh failure clears the token and calls the redirect callback once. + +- [ ] **Step 6: Implement the Umi request configuration** + +Export: + +```ts +export const buildFlowRequestConfig: RequestConfig = { + baseURL: '/api', + withCredentials: true, + requestInterceptors: [attachBuildFlowHeaders], + responseInterceptors: [[unwrapBuildFlowResponse, handleBuildFlowResponseError]], + errorConfig: { errorHandler: showBuildFlowError }, +}; +``` + +Rules: + +- JSON envelopes are unwrapped by replacing `response.data` with `envelope.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 that `App.useApp()` registers from a mounted component. + +Implement and mount this component from `layout.childrenRender`: + +```tsx +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: + +```ts +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`: + +```ts +export function request( + url: string, + options?: BuildFlowRequestOptions, +): Promise { + return umiRequest(url, options); +} +``` + +Regenerate and run `npm run api:check`. + +- [ ] **Step 8: Verify and commit the request core** + +Run: + +```bash +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: + +```bash +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: + +```ts +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: + +```ts +export async function downloadFile( + url: string, + options?: BuildFlowRequestOptions, +): Promise<{ blob: Blob; fileName?: string }>; + +export async function uploadFormData( + url: string, + formData: FormData, + options?: BuildFlowRequestOptions, +): Promise; +``` + +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: + +```ts +export interface SseEvent { + event: string; + data: string; + id?: string; +} + +export async function* streamSse( + url: string, + init: RequestInit = {}, +): AsyncGenerator; +``` + +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: + +```ts +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** + +```bash +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: + +```ts +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`, `desc` map to `id`, `name`, `description`; +- missing arrays become empty arrays; +- `loadAuthenticatedState()` requests current user, menu, and access codes concurrently; +- logout calls `/auth/logout` with credentials and clears the token in `finally`. + +- [ ] **Step 3: Implement the auth adapter** + +Public API: + +```ts +export async function login(credentials: LoginCredentials): Promise; +export async function logout(): Promise; +export async function getCurrentUser(): Promise; +export async function getCurrentUserMenus(): Promise; +export async function getAccessCodes(): Promise; +export async function loadAuthenticatedState(): Promise; +``` + +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: + +```ts +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: + +```ts +{ + currentUser?: CurrentUser; + menuRoutes: MenuRoute[]; + accessCodes: string[]; + fetchAuthenticatedState: () => Promise; + settings: Partial; + 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: + +```ts +{ + 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: + +```ts +{ + 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: + +```tsx +} + 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: + +```bash +npm run api:generate:template +``` + +Confirm: + +```bash +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** + +```bash +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: + +```js +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: + +1. anonymous Swagger is 200 and contains required paths; +2. empty login is 400 `invalid_param`; +3. bad credentials are 401 `unauthorized`; +4. successful login is code 0, returns access token, and sets HttpOnly `jwt`; +5. current user, menu, and access codes succeed with bearer token; +6. menu node count is at least 30 and access-code count is at least 83; +7. refresh returns non-empty text and rotates the cookie; +8. the old refresh cookie is rejected; +9. logout clears/revokes the rotated cookie; +10. 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: + +```json +"test:smoke": "node scripts/api-smoke.mjs" +``` + +Run with the Testing Host active: + +```bash +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** + +```bash +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: + +```ts +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: + +```ts +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** + +```bash +npx playwright install chromium +npm run test:e2e +``` + +Add: + +```json +"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** + +```bash +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.md` only 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: + +1. require `scripts/test-infra.sh up` to have succeeded; +2. start `ASPNETCORE_ENVIRONMENT=Testing dotnet run --project src/BuildFlow.Host --no-build`; +3. store only the PID under `${TMPDIR:-/tmp}/buildflow-testing-host.pid`; +4. wait up to 120 seconds for `http://127.0.0.1:5667/health`; +5. 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: + +```json +"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.json` only; +- backend port 5667 only; +- `npm run api:sync` requires a running backend, while build and `api:check` do 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: + +```bash +# 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: + +```bash +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** + +```bash +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: + +```bash +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: + +```bash +git add package.json package-lock.json docs/BACKEND_INTEGRATION.md docs/INTEGRATION_SOP.md +git commit -m "docs: add buildflow integration sop" +``` + +Backend: + +```bash +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.