完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)
这个提交包含在:
@@ -16,3 +16,80 @@ task before modifying approval behavior.
|
||||
not drift because rules changed later.
|
||||
- Reviewer review is required after changes touching workflow, status machine,
|
||||
approval rules, permissions or migrations.
|
||||
|
||||
## Scenario: System Operator Is Not A Business Approver
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: adding or changing OA account administration, role assignment,
|
||||
approval eligibility, payment review or emergency-superuser behavior.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Daily responsibility roles: `manager`, `ceo`, `finance`, `system_admin`.
|
||||
- Emergency role: `admin` (`*`), hidden from ordinary assignment UI.
|
||||
- System permissions: `system.accounts.manage`, `system.roles.manage`.
|
||||
- Payment reviewer permission: `payment.batch.review`.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- `system_admin` manages ordinary accounts and system configuration only; it
|
||||
does not imply leadership, finance, payroll, personnel-sensitive access or
|
||||
business approval authority.
|
||||
- A payment reviewer must have both `payment.batch.review` and an explicit
|
||||
`manager` or `ceo` role. `admin` or `system_admin` alone is not a normal
|
||||
reviewer.
|
||||
- `manager` does not imply that every employee reports to that account;
|
||||
reporting scope remains in `direct_manager_id`.
|
||||
- A non-emergency system administrator cannot edit their own roles or grant,
|
||||
revoke or import `admin` / `system_admin`. Role changes remain audited.
|
||||
- Employee aggregate endpoints must not treat `system.settings` as personnel-
|
||||
sensitive authority. Salary summaries require `payroll.manage`; performance
|
||||
summaries require `system.users_sensitive`; unauthorized branches issue no
|
||||
salary/performance query and return only the non-sensitive summary shape.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- System administrator changes own roles -> `403 PERM_001` before role writes.
|
||||
- System administrator changes a protected role -> `403 PERM_001`, zero role
|
||||
and audit writes.
|
||||
- Reviewer has permission but lacks `manager|ceo` -> `403 PERM_001`.
|
||||
- Batch creator attempts review -> deterministic `409 VALID_001`.
|
||||
- `system_admin` opens ordinary employee statistics -> salary is null,
|
||||
performance is empty and both sensitive query families remain untouched.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: one person may explicitly hold both `manager` and `system_admin`, while
|
||||
each route evaluates the responsibility required for that action.
|
||||
- Base: `system_admin` creates or disables an ordinary account without seeing
|
||||
payroll, bank, identity or employee-contract content.
|
||||
- Bad: treat `admin` as the default company leader or use `system_admin` as an
|
||||
approval fallback because the team is small.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Role matrix for employee, manager, CEO, finance, system administrator and
|
||||
emergency administrator.
|
||||
- Negative tests for self-role change, protected-role assignment/import and
|
||||
permission-only payment review.
|
||||
- Negative employee-statistics test using the real `system_admin` permission
|
||||
set, plus positive tests for explicit payroll/personnel-sensitive authority.
|
||||
- Reviewer read-only review after changing role or approval semantics.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```js
|
||||
if (hasPermission(actor, "*")) allowPaymentReview();
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```js
|
||||
if (!hasPermission(actor, "payment.batch.review")
|
||||
|| !actor.roleCodes.some((role) => role === "manager" || role === "ceo")) {
|
||||
throw forbidden();
|
||||
}
|
||||
```
|
||||
|
||||
@@ -383,3 +383,82 @@ const currentSession = await tx.authSession.findUnique({ where: { id: sessionId
|
||||
if (!currentSession || currentSession.revoked_at) return invalidRefresh();
|
||||
await rotateSession(tx, currentSession);
|
||||
```
|
||||
|
||||
## Scenario: Small-Team Payment Batch Consistency
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: changing payment-batch creation, review, execution, retry,
|
||||
cancellation, receipt evidence or the legacy direct-payment path.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Candidate API: `GET /api/payment-batches/candidates?q=&page=&pageSize=`.
|
||||
- State APIs: `POST /api/payment-batches/:id/review`,
|
||||
`POST /api/payment-batches/:id/execute`,
|
||||
`POST /api/payment-batches/:id/cancel` with `{ reason }`.
|
||||
- Receipt invalidation accepts `{ reason }` and retains the record.
|
||||
- DB: `payment_batches.cancelled_reason TEXT NULL`.
|
||||
- Isolated concurrency gate:
|
||||
`bash scripts/qa/run-web-gates.sh payment-concurrency-local`.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Normal flow is finance maker/executor plus one independent `manager|ceo`
|
||||
reviewer; the maker cannot review their own batch.
|
||||
- Lock the batch before state checks, then lock source entities in stable order
|
||||
and recheck status, amount and reservation inside the same transaction.
|
||||
- When `payment_batch_v1` is enabled, an approved reimbursement can enter only
|
||||
one active batch and the legacy direct-payment route must not form a second
|
||||
write path.
|
||||
- Retry processes only `pending|failed` items. Existing `executed` items never
|
||||
repeat payment writes, follow-ups or notifications. Persisted execution
|
||||
summary is recomputed from every final item state; the audit separately
|
||||
records the current attempt.
|
||||
- Cancellation requires a 2-500 character reason. Receipt evidence is voided
|
||||
with audit history, not physically deleted.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Missing review, maker-reviewer conflict or terminal state -> `409 VALID_001`.
|
||||
- Concurrent loser after a valid winner -> deterministic `409`, zero duplicate
|
||||
state, audit or notification writes.
|
||||
- Missing/invalid cancellation or void reason -> `400 VALID_001`.
|
||||
- Source already paid, amount changed or reserved by another active batch ->
|
||||
`409 VALID_001` and no partial batch mutation.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: one executor wins, a concurrent cancel or second executor receives
|
||||
`409`, and the final payment/audit/notification counts remain one.
|
||||
- Base: a failed batch retries only failed items and reaches `executed` with an
|
||||
all-item final summary.
|
||||
- Bad: count only the retry attempt in `execution_summary_json`, or allow the
|
||||
direct route to pay while the same source is in a valid batch.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Focused route tests for candidate authority, maker-checker, retry summary,
|
||||
cancellation, receipt void and zero-side-effect refusals.
|
||||
- Real disposable PostgreSQL races for double execute, execute/cancel, double
|
||||
create and direct-payment/batch competition; assert unique audits and
|
||||
notifications and zero residual test databases.
|
||||
- Prisma validate, clean empty-database migration application and a read-only
|
||||
reviewer after payment-state changes.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```js
|
||||
summary.successCount = successesInThisRetry;
|
||||
batch.status = failuresInThisRetry ? "failed" : "executed";
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```js
|
||||
const finalSummary = summarizeFinalItemStatuses(finalItemStatuses);
|
||||
batch.execution_summary_json = finalSummary;
|
||||
batch.status = finalSummary.failedCount > 0 ? "failed" : "executed";
|
||||
```
|
||||
|
||||
@@ -442,3 +442,77 @@ validate_isolated_target_before_writes
|
||||
run_mutating_auth_regression
|
||||
drop_owned_database_or_fail
|
||||
```
|
||||
|
||||
## Scenario: Repository Local Candidate Gate
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: declaring a checkout ready for internal deployment planning without
|
||||
deploying it or using public, production or real-account evidence.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Full gate: `bash scripts/qa/run-local-candidate.sh full`.
|
||||
- Browser-only rerun: `bash scripts/qa/run-local-candidate.sh focused`.
|
||||
- Parse-only check: `bash scripts/qa/run-local-candidate.sh list`.
|
||||
- Route contract: `scripts/qa/route-manifest.json`.
|
||||
- Result verifier: `scripts/qa/verify-local-candidate-results.mjs`.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- `full` alone may print `LOCAL_CANDIDATE_PASS`; `focused` prints
|
||||
`FOCUSED_BROWSER_PASS`, and `list` prints `LIST_ONLY`.
|
||||
- The gate accepts only loopback HTTP origins and canonical TCP ports. It uses
|
||||
local synthetic fixtures, excludes `manual/live` specs and does not consume
|
||||
historical Issue CSV as a candidate prerequisite.
|
||||
- Every registered route is classified. Every core bundle has one stable,
|
||||
source-bound witness; candidate spec basenames are unique.
|
||||
- Every selected Playwright result must pass. Missing, duplicate, unknown,
|
||||
skipped, multi-tag or wrong-source witnesses and any failed/skipped
|
||||
acceptance test make the gate fail.
|
||||
- The script's full pass proves only its local scope. A task changing payment
|
||||
concurrency also requires the isolated PostgreSQL payment gate; deployment,
|
||||
public checks and real-device behavior remain separate states.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Non-loopback URL, credentialed URL, path/query target or invalid port -> fail
|
||||
before Vite/Playwright use.
|
||||
- Unclassified/orphan route or missing core behavior/visual witness -> non-zero
|
||||
scanner result.
|
||||
- Playwright suite error, unexpected, failed or skipped test -> non-zero result
|
||||
gate with basename-only redacted diagnostics.
|
||||
- `focused` succeeds -> useful rerun evidence, but not a full candidate claim.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: full base checks pass, all source-bound witnesses and acceptance specs
|
||||
execute, and the runner prints one full-pass label.
|
||||
- Base: use `list` before a long run to prove assets parse, while recording that
|
||||
no behavior has run.
|
||||
- Bad: reuse a prior browser pass after parser changes, or report focused/list
|
||||
output as `LOCAL_CANDIDATE_PASS`.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Negative tests for route gaps, manual/live evidence, duplicate basenames,
|
||||
invalid loopback inputs, witness result/source drift and mode labels.
|
||||
- Current-checkout `full` run with zero skipped/unexpected results.
|
||||
- For payment-state changes, run the disposable PostgreSQL gate and prove
|
||||
cleanup left no matching temporary databases.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```bash
|
||||
bash scripts/qa/run-local-candidate.sh list
|
||||
echo LOCAL_CANDIDATE_PASS
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```bash
|
||||
bash scripts/qa/run-local-candidate.sh full
|
||||
# Only this successful branch emits LOCAL_CANDIDATE_PASS.
|
||||
```
|
||||
|
||||
@@ -938,6 +938,69 @@ validation prevent repeated re-entry and reduce support/debugging cost.
|
||||
- Focused Playwright coverage for draft save, submit validation, unsaved-change
|
||||
protection and server-validation failure paths when changing form behavior.
|
||||
|
||||
## Scenario: Data-Router Unsaved-Change Protection
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: a React form can lose unsaved work through in-app navigation,
|
||||
browser back/forward, reload, closing its panel or switching records.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Router: `createBrowserRouter(...)` rendered through `RouterProvider`.
|
||||
- Navigation blocker: React Router `useBlocker(isDirty)`.
|
||||
- Reload/close guard: `beforeunload` while `isDirty === true`.
|
||||
- Shared UI contract: one unsaved-change dialog with continue-editing and
|
||||
discard actions.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Block navigation before the route or selected record changes; do not restore
|
||||
old UI state after navigation has already occurred.
|
||||
- `blocker.reset()` keeps the current route and form values. `blocker.proceed()`
|
||||
is called only after explicit discard confirmation.
|
||||
- Record switching and panel closing use the same pending-action contract as
|
||||
route navigation. Successful save or explicit discard clears dirty state.
|
||||
- Register `beforeunload` only while dirty and remove it during cleanup.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Clean form navigation -> continue without a dialog.
|
||||
- Dirty in-app navigation or browser back -> block and show one dialog.
|
||||
- Continue editing -> URL, record selection and values remain unchanged.
|
||||
- Discard -> execute exactly one pending navigation/selection and clear dirty.
|
||||
- Reload/close while dirty -> browser-level unload warning remains active.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: reimbursement, procurement, contract and asset forms reuse one guard
|
||||
while keeping their own save and dirty-field logic.
|
||||
- Base: a short non-editable filter form does not register the guard.
|
||||
- Bad: listen to history after navigation, then push the previous URL back; the
|
||||
user can see state flicker or lose form data before confirmation.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Playwright covers link navigation, browser back, reload and record switch.
|
||||
- Assert cancel preserves URL and values; discard changes target once; save
|
||||
removes the warning.
|
||||
- Frontend typecheck and build after router or shared guard changes.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```ts
|
||||
window.addEventListener("popstate", () => navigate(previousUrl));
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```ts
|
||||
const blocker = useBlocker(isDirty);
|
||||
// reset to keep editing; proceed only after explicit discard.
|
||||
```
|
||||
|
||||
## Convention: Detail Evidence Layout
|
||||
|
||||
**What**: Detail pages use one evidence-first structure so users can understand
|
||||
|
||||
在新工单中引用
屏蔽一个用户