沉淀生产收口规约与清理证据(07-15-production-trust-closeout-cleanup)

这个提交包含在:
2026-07-16 05:18:59 +08:00
父节点 5fb91803fa
当前提交 79bf6cff83
共修改 12 个文件,包含 784 行新增和 3 行删除
@@ -169,3 +169,128 @@ if (acknowledgementCount > 0) {
throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 409 });
}
```
## Scenario: Safe Development Seed
### 1. Scope / Trigger
- Trigger: changing or running `server/src/scripts/seed-dev.js`, its safety
guard, development account defaults or the `seed:dev` npm command.
### 2. Signatures
- Command: `SEED_DEV_ALLOW=1 SEED_DEV_DATABASE_CONFIRM=<db> npm run seed:dev`.
- Optional reset: `SEED_DEV_RESET_PASSWORDS=1`.
- Target: `DATABASE_URL` parsed with the same `pg` semantics used by Prisma's
PostgreSQL adapter.
### 3. Contracts
- `NODE_ENV=production` is permanently refused.
- The explicit allow switch and exact database-name confirmation are both
required.
- The effective PostgreSQL host, including query-string overrides, must be
loopback; checking only the URI authority is insufficient.
- Existing passwords are preserved by default. Newly created accounts and
explicitly rotated accounts set `must_change_password=true`.
- The guard and seed logs never print credentials, complete targets or account
passwords.
### 4. Validation & Error Matrix
- Production -> `production_forbidden`.
- Missing allow switch -> `explicit_allow_required`.
- Invalid/non-PostgreSQL URL -> `database_url_invalid` or
`database_protocol_invalid`.
- Effective non-loopback host, including `?host=...` ->
`database_host_not_local`.
- Missing/mismatched confirmation -> `database_confirmation_mismatch`.
### 5. Good/Base/Bad Cases
- Good: loopback test database, exact confirmation, new accounts require a
password change.
- Base: rerun without reset preserves existing password hashes.
- Bad: accept `postgresql://[email protected]/db?host=remote.internal` because only
`URL.hostname` was checked.
### 6. Tests Required
- Pure guard tests for production, missing allow, remote authority, host-query
override, exact confirmation and redacted errors.
- Unit tests for existing-password preservation, explicit rotation and new
account first-login change.
- Do not validate refusal paths by connecting to a real database.
### 7. Wrong vs Correct
#### Wrong
```js
if (new URL(env.DATABASE_URL).hostname === "127.0.0.1") allowSeed();
```
#### Correct
```js
const client = new Client({ connectionString: env.DATABASE_URL });
if (!LOOPBACK_HOSTS.has(client.host)) refuse("database_host_not_local");
```
## Scenario: Database-Free App Contract Tests
### 1. Scope / Trigger
- Trigger: unit tests call the production `buildApp()` factory to verify health,
readiness, static serving or other app-level contracts in CI without a DB.
### 2. Signatures
- Production: `buildApp()`.
- Test seam: `buildApp({ prismaPlugin, startJobs })`.
### 3. Contracts
- Omitted dependencies always use the production Prisma plugin and job starter.
- Unit tests inject a `fastify-plugin`-wrapped Prisma decorator and a no-op job
starter; they must not rely on ESM `vi.mock` intercepting a CJS `require`.
- App-level test doubles expose only methods used by the tested requests.
- `DATABASE_URL='' JWT_SECRET=''` must not make isolated app tests access a
developer `.env` or local PostgreSQL instance.
### 4. Validation & Error Matrix
- Production call with missing DB target -> normal startup refusal.
- Test call with injected Prisma plugin -> no production client construction.
- Test double missing a method exercised by the request -> focused unit failure.
- Injected dependencies leaking into `buildApp()` callers without options ->
contract regression.
### 5. Good/Base/Bad Cases
- Good: health/static tests inject Prisma and pass with empty DB/JWT process
variables after setting their own test JWT.
- Base: API/Worker call `buildApp()` without options and retain production wiring.
- Bad: a hoisted `vi.mock("../prismaClient")` appears green only because a local
`.env` database is reachable.
### 6. Tests Required
- Run health and static tests with `DATABASE_URL='' JWT_SECRET=''`.
- Run the full `scripts/check-minimal.sh` under the same empty environment.
- Keep the production default path covered by normal startup/smoke checks.
### 7. Wrong vs Correct
#### Wrong
```js
vi.mock("../../src/prismaClient", () => ({ createPrismaClient: fake }));
await buildApp(); // CJS plugin may still construct the real client
```
#### Correct
```js
await buildApp({ prismaPlugin: testPrismaPlugin, startJobs: noOpStartJobs });
```