diff --git a/.agent/rules/frontend/frontend.md b/.agent/rules/frontend/frontend.md
index 068062526..f93261a11 100644
--- a/.agent/rules/frontend/frontend.md
+++ b/.agent/rules/frontend/frontend.md
@@ -74,6 +74,7 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
- **Errors are handled globally**—`shared-webapp/infrastructure/http/errorHandler.ts` automatically shows toast notifications with the server's error message (don't manually show toasts for errors)
- **Validation errors**: Pass to forms via `validationErrors={mutation.error?.errors}`
- **`onError` is for UI cleanup only** (resetting loading states, closing dialogs), not for showing errors
+ - **Toast notifications**: Show success toasts in mutation `onSuccess` callbacks, not in `useEffect` watching `isSuccess` (avoids React effect scheduling delays)
4. Responsive design utilities:
- Use `useViewportResize()` hook to detect mobile viewport (returns `true` when mobile)
@@ -92,16 +93,21 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
- `z-[200]`: Mobile full-screen menus
- Note: Dropdowns, tooltips, and popovers use React Aria's overlay system which manages stacking relative to their context
-6. Always follow these steps when implementing changes:
+6. DirtyModal close handlers:
+ - **X button**: Use Dialog's `close` from render prop (shows unsaved warning if dirty)
+ - **Cancel button**: Use `handleCancel` that clears state and closes immediately (bypasses warning)
+ - Always clear dirty state in `onSuccess` and `onCloseComplete`
+
+7. Always follow these steps when implementing changes:
- Consult relevant rule files and list which ones guided your implementation
- Search the codebase for similar code before implementing new code
- Reference existing implementations to maintain consistency
-7. Build and format your changes:
+8. Build and format your changes:
- After each minor change, use the **execute MCP tool** with `command: "build"` for frontend
- This ensures consistent code style across the codebase
-8. Verify your changes:
+9. Verify your changes:
- When a feature is complete, run these MCP tools for frontend in sequence: **build**, **format**, **inspect**
- **ALL inspect findings are blocking** - CI pipeline fails on any result marked "Issues found"
- Severity level (note/warning/error) is irrelevant - fix all findings before proceeding
@@ -111,39 +117,49 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
```tsx
// ✅ DO: Correct patterns
-export function UserPicker({ isOpen, isPending, onOpenChange }: UserPickerProps) {
+export function UserPicker({ isOpen, onOpenChange }: UserPickerProps) {
+ const [isFormDirty, setIsFormDirty] = useState(false);
const { data } = api.useQuery("get", "/api/account-management/users", { enabled: isOpen });
const activeUsers = (data?.users ?? []).filter((u) => u.isActive); // ✅ Compute derived values inline
- const handleChangeSelection = (keys: Selection) => { /* ... */ }; // ✅ handleVerbNoun pattern
+ const inviteMutation = api.useMutation("post", "/api/account-management/users/invite", {
+ onSuccess: () => { // ✅ Show toast in onSuccess (not useEffect)
+ setIsFormDirty(false);
+ toastQueue.add({ title: t`Success`, description: t`User invited`, variant: "success" });
+ onOpenChange(false);
+ }
+ });
+
+ const handleCloseComplete = () => setIsFormDirty(false);
+ const handleCancel = () => { setIsFormDirty(false); onOpenChange(false); }; // ✅ Clear state + close (bypasses warning)
return (
- // ✅ Prevent dismiss during pending
+
-
+
);
}
@@ -152,11 +168,19 @@ function BadUserDialog({ users, selectedId, isOpen, onClose }) {
const [filteredUsers, setFilteredUsers] = useState([]); // ❌ State for derived values
const [isAdmin, setIsAdmin] = useState(false); // ❌ Duplicate state that can be calculated
+ const inviteMutation = api.useMutation("post", "/api/users/invite");
+
useEffect(() => { // ❌ useEffect for calculations - compute inline instead
setFilteredUsers(users.filter(u => u.isActive));
setIsAdmin(users.some(u => u.id === selectedId && u.role === "admin")); // ❌ Hardcode strings - use API contract types
}, [users, selectedId]);
+ useEffect(() => { // ❌ useEffect watching isSuccess causes toast timing issues
+ if (inviteMutation.isSuccess) {
+ toastQueue.add({ title: "Success", variant: "success" });
+ }
+ }, [inviteMutation.isSuccess]);
+
const getDisplayName = useCallback((user) => { // ❌ Premature useCallback without performance need
return `${user.firstName} ${user.lastName}`;
}, []);
@@ -166,17 +190,25 @@ function BadUserDialog({ users, selectedId, isOpen, onClose }) {
return (
// ❌ Missing isDismissable={!isPending}
);
diff --git a/.agent/workflows/process/review-end-to-end-tests.md b/.agent/workflows/process/review-end-to-end-tests.md
index 77fb68531..d247222bd 100644
--- a/.agent/workflows/process/review-end-to-end-tests.md
+++ b/.agent/workflows/process/review-end-to-end-tests.md
@@ -43,11 +43,12 @@ You are reviewing: **{{{title}}}**
{
"todos": [
{"content": "Read [feature] and [task] to understand requirements", "status": "pending", "activeForm": "Reading feature and task"},
- {"content": "Run e2e tests and verify ALL pass with zero tolerance", "status": "pending", "activeForm": "Running E2E tests"},
+ {"content": "Run feature-specific e2e tests", "status": "pending", "activeForm": "Running feature E2E tests"},
{"content": "Review test file structure and organization", "status": "pending", "activeForm": "Reviewing test structure"},
{"content": "Review each test step for correct patterns", "status": "pending", "activeForm": "Reviewing test steps"},
{"content": "Review test efficiency and speed", "status": "pending", "activeForm": "Reviewing test efficiency"},
{"content": "Make binary decision (approve or reject)", "status": "pending", "activeForm": "Making decision"},
+ {"content": "If approved, run full regression test suite", "status": "pending", "activeForm": "Running full regression tests"},
{"content": "If approved, commit changes", "status": "pending", "activeForm": "Committing if approved"},
{"content": "Update [task] status to [Completed] or [Active]", "status": "pending", "activeForm": "Updating task status"},
{"content": "MANDATORY: Call CompleteWork", "status": "pending", "activeForm": "Calling CompleteWork"}
@@ -76,13 +77,13 @@ You are reviewing: **{{{title}}}**
- Read [End-to-End Tests](/.agent/rules/end-to-end-tests/end-to-end-tests.md)
- Ensure engineer followed all patterns
-**STEP 2**: Run e2e tests and verify ALL pass with zero tolerance
+**STEP 2**: Run feature-specific e2e tests first
**If tests require backend changes, run the run tool first**:
- Use **run MCP tool** to restart server and run migrations
- The tool starts .NET Aspire at https://localhost:9000
-**Run E2E tests**:
+**Run feature-specific E2E tests**:
- Use **end-to-end MCP tool** to run tests: `end-to-end(searchTerms=["feature-name"])`
- **ALL tests MUST pass with ZERO failures to approve**
- **Verify ZERO console errors** during test execution
@@ -150,7 +151,15 @@ You are reviewing: **{{{title}}}**
**When rejecting:** Do full review first, then reject with ALL issues listed (avoid multiple rounds).
-**STEP 7**: If approved, commit changes
+**STEP 7**: If approved, run full regression test suite
+
+**Before committing, run all e2e tests to ensure no regressions:**
+- Use **end-to-end MCP tool** WITHOUT searchTerms: `end-to-end()`
+- This runs the complete test suite across all browsers
+- **ALL tests MUST pass with ZERO failures**
+- If ANY test fails: REJECT (do not commit)
+
+**STEP 8**: Commit changes
1. Stage test files: `git add ` for each test file
2. Commit: One line, imperative form, no description, no co-author
@@ -158,7 +167,7 @@ You are reviewing: **{{{title}}}**
Don't use `git add -A` or `git add .`
-**STEP 8**: Update [task] status to [Completed] or [Active]
+**STEP 9**: Update [task] status to [Completed] or [Active]
**If `featureId` is NOT "ad-hoc" (regular task from a feature):**
- If APPROVED: Update [task] status to [Completed].
@@ -167,7 +176,7 @@ Don't use `git add -A` or `git add .`
**If `featureId` is "ad-hoc" (ad-hoc work):**
- Skip [PRODUCT_MANAGEMENT_TOOL] status updates.
-**STEP 9**: Call CompleteWork
+**STEP 10**: Call CompleteWork
**Call MCP CompleteWork tool**:
- `mode`: "review"
diff --git a/.claude/agentic-workflow/system-prompts/qa-engineer.txt b/.claude/agentic-workflow/system-prompts/qa-engineer.txt
index 0022199c4..a40db8eae 100644
--- a/.claude/agentic-workflow/system-prompts/qa-engineer.txt
+++ b/.claude/agentic-workflow/system-prompts/qa-engineer.txt
@@ -71,3 +71,11 @@ Do not report (these are not system bugs):
If you recover from a problem: Report problem + solution (2 calls).
Report system bugs only. Every unreported workflow bug makes the agentic system worse.
+
+## Flaky Test Tracking
+
+**Run `/update-flaky-tests` immediately when:**
+- Test failures occur that are unrelated to your current changes - log them
+- You fix a known flaky test - mark it as fix-applied in the tracker
+
+The flaky test tracker helps the team understand which tests are unreliable. Always update it.
diff --git a/.claude/agentic-workflow/system-prompts/qa-reviewer.txt b/.claude/agentic-workflow/system-prompts/qa-reviewer.txt
index a2d3c2a67..95aafebf7 100644
--- a/.claude/agentic-workflow/system-prompts/qa-reviewer.txt
+++ b/.claude/agentic-workflow/system-prompts/qa-reviewer.txt
@@ -69,3 +69,11 @@ Do not report (these are not system bugs):
If you recover from a problem: Report problem + solution (2 calls).
Report system bugs only. Every unreported workflow bug makes the agentic system worse.
+
+## Flaky Test Tracking
+
+**Run `/update-flaky-tests` immediately when:**
+- Test failures occur that are unrelated to the engineer's changes - log them
+- You commit a fix for a known flaky test - mark it as fix-applied in the tracker
+
+The flaky test tracker helps the team understand which tests are unreliable. Always update it.
diff --git a/.claude/commands/process/review-end-to-end-tests.md b/.claude/commands/process/review-end-to-end-tests.md
index 4f90b3918..f301670d3 100644
--- a/.claude/commands/process/review-end-to-end-tests.md
+++ b/.claude/commands/process/review-end-to-end-tests.md
@@ -48,11 +48,12 @@ You are reviewing: **{{{title}}}**
{
"todos": [
{"content": "Read [feature] and [task] to understand requirements", "status": "pending", "activeForm": "Reading feature and task"},
- {"content": "Run e2e tests and verify ALL pass with zero tolerance", "status": "pending", "activeForm": "Running E2E tests"},
+ {"content": "Run feature-specific e2e tests", "status": "pending", "activeForm": "Running feature E2E tests"},
{"content": "Review test file structure and organization", "status": "pending", "activeForm": "Reviewing test structure"},
{"content": "Review each test step for correct patterns", "status": "pending", "activeForm": "Reviewing test steps"},
{"content": "Review test efficiency and speed", "status": "pending", "activeForm": "Reviewing test efficiency"},
{"content": "Make binary decision (approve or reject)", "status": "pending", "activeForm": "Making decision"},
+ {"content": "If approved, run full regression test suite", "status": "pending", "activeForm": "Running full regression tests"},
{"content": "If approved, commit changes", "status": "pending", "activeForm": "Committing if approved"},
{"content": "Update [task] status to [Completed] or [Active]", "status": "pending", "activeForm": "Updating task status"},
{"content": "MANDATORY: Call CompleteWork", "status": "pending", "activeForm": "Calling CompleteWork"}
@@ -81,13 +82,13 @@ You are reviewing: **{{{title}}}**
- Read [End-to-End Tests](/.claude/rules/end-to-end-tests/end-to-end-tests.md)
- Ensure engineer followed all patterns
-**STEP 2**: Run e2e tests and verify ALL pass with zero tolerance
+**STEP 2**: Run feature-specific e2e tests first
**If tests require backend changes, run the run tool first**:
- Use **run MCP tool** to restart server and run migrations
- The tool starts .NET Aspire at https://localhost:9000
-**Run E2E tests**:
+**Run feature-specific E2E tests**:
- Use **end-to-end MCP tool** to run tests: `end-to-end(searchTerms=["feature-name"])`
- **ALL tests MUST pass with ZERO failures to approve**
- **Verify ZERO console errors** during test execution
@@ -155,7 +156,15 @@ You are reviewing: **{{{title}}}**
**When rejecting:** Do full review first, then reject with ALL issues listed (avoid multiple rounds).
-**STEP 7**: If approved, commit changes
+**STEP 7**: If approved, run full regression test suite
+
+**Before committing, run all e2e tests to ensure no regressions:**
+- Use **end-to-end MCP tool** WITHOUT searchTerms: `end-to-end()`
+- This runs the complete test suite across all browsers
+- **ALL tests MUST pass with ZERO failures**
+- If ANY test fails: REJECT (do not commit)
+
+**STEP 8**: Commit changes
1. Stage test files: `git add ` for each test file
2. Commit: One line, imperative form, no description, no co-author
@@ -163,7 +172,7 @@ You are reviewing: **{{{title}}}**
Don't use `git add -A` or `git add .`
-**STEP 8**: Update [task] status to [Completed] or [Active]
+**STEP 9**: Update [task] status to [Completed] or [Active]
**If `featureId` is NOT "ad-hoc" (regular task from a feature):**
- If APPROVED: Update [task] status to [Completed].
@@ -172,7 +181,7 @@ Don't use `git add -A` or `git add .`
**If `featureId` is "ad-hoc" (ad-hoc work):**
- Skip [PRODUCT_MANAGEMENT_TOOL] status updates.
-**STEP 9**: Call CompleteWork
+**STEP 10**: Call CompleteWork
**Call MCP CompleteWork tool**:
- `mode`: "review"
diff --git a/.claude/rules/frontend/frontend.md b/.claude/rules/frontend/frontend.md
index 218221d22..07ded578a 100644
--- a/.claude/rules/frontend/frontend.md
+++ b/.claude/rules/frontend/frontend.md
@@ -74,6 +74,7 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
- **Errors are handled globally**—`shared-webapp/infrastructure/http/errorHandler.ts` automatically shows toast notifications with the server's error message (don't manually show toasts for errors)
- **Validation errors**: Pass to forms via `validationErrors={mutation.error?.errors}`
- **`onError` is for UI cleanup only** (resetting loading states, closing dialogs), not for showing errors
+ - **Toast notifications**: Show success toasts in mutation `onSuccess` callbacks, not in `useEffect` watching `isSuccess` (avoids React effect scheduling delays)
4. Responsive design utilities:
- Use `useViewportResize()` hook to detect mobile viewport (returns `true` when mobile)
@@ -92,16 +93,21 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
- `z-[200]`: Mobile full-screen menus
- Note: Dropdowns, tooltips, and popovers use React Aria's overlay system which manages stacking relative to their context
-6. Always follow these steps when implementing changes:
+6. DirtyModal close handlers:
+ - **X button**: Use Dialog's `close` from render prop (shows unsaved warning if dirty)
+ - **Cancel button**: Use `handleCancel` that clears state and closes immediately (bypasses warning)
+ - Always clear dirty state in `onSuccess` and `onCloseComplete`
+
+7. Always follow these steps when implementing changes:
- Consult relevant rule files and list which ones guided your implementation
- Search the codebase for similar code before implementing new code
- Reference existing implementations to maintain consistency
-7. Build and format your changes:
+8. Build and format your changes:
- After each minor change, use the **execute MCP tool** with `command: "build"` for frontend
- This ensures consistent code style across the codebase
-8. Verify your changes:
+9. Verify your changes:
- When a feature is complete, run these MCP tools for frontend in sequence: **build**, **format**, **inspect**
- **ALL inspect findings are blocking** - CI pipeline fails on any result marked "Issues found"
- Severity level (note/warning/error) is irrelevant - fix all findings before proceeding
@@ -111,39 +117,49 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
```tsx
// ✅ DO: Correct patterns
-export function UserPicker({ isOpen, isPending, onOpenChange }: UserPickerProps) {
+export function UserPicker({ isOpen, onOpenChange }: UserPickerProps) {
+ const [isFormDirty, setIsFormDirty] = useState(false);
const { data } = api.useQuery("get", "/api/account-management/users", { enabled: isOpen });
const activeUsers = (data?.users ?? []).filter((u) => u.isActive); // ✅ Compute derived values inline
- const handleChangeSelection = (keys: Selection) => { /* ... */ }; // ✅ handleVerbNoun pattern
+ const inviteMutation = api.useMutation("post", "/api/account-management/users/invite", {
+ onSuccess: () => { // ✅ Show toast in onSuccess (not useEffect)
+ setIsFormDirty(false);
+ toastQueue.add({ title: t`Success`, description: t`User invited`, variant: "success" });
+ onOpenChange(false);
+ }
+ });
+
+ const handleCloseComplete = () => setIsFormDirty(false);
+ const handleCancel = () => { setIsFormDirty(false); onOpenChange(false); }; // ✅ Clear state + close (bypasses warning)
return (
- // ✅ Prevent dismiss during pending
+
-
+
);
}
@@ -152,11 +168,19 @@ function BadUserDialog({ users, selectedId, isOpen, onClose }) {
const [filteredUsers, setFilteredUsers] = useState([]); // ❌ State for derived values
const [isAdmin, setIsAdmin] = useState(false); // ❌ Duplicate state that can be calculated
+ const inviteMutation = api.useMutation("post", "/api/users/invite");
+
useEffect(() => { // ❌ useEffect for calculations - compute inline instead
setFilteredUsers(users.filter(u => u.isActive));
setIsAdmin(users.some(u => u.id === selectedId && u.role === "admin")); // ❌ Hardcode strings - use API contract types
}, [users, selectedId]);
+ useEffect(() => { // ❌ useEffect watching isSuccess causes toast timing issues
+ if (inviteMutation.isSuccess) {
+ toastQueue.add({ title: "Success", variant: "success" });
+ }
+ }, [inviteMutation.isSuccess]);
+
const getDisplayName = useCallback((user) => { // ❌ Premature useCallback without performance need
return `${user.firstName} ${user.lastName}`;
}, []);
@@ -166,17 +190,25 @@ function BadUserDialog({ users, selectedId, isOpen, onClose }) {
return (
// ❌ Missing isDismissable={!isPending}
);
diff --git a/.claude/skills/update-flaky-tests/SKILL.md b/.claude/skills/update-flaky-tests/SKILL.md
new file mode 100644
index 000000000..5537ece02
--- /dev/null
+++ b/.claude/skills/update-flaky-tests/SKILL.md
@@ -0,0 +1,153 @@
+---
+name: update-flaky-tests
+description: Update the flaky test tracker. Use when you encounter test failures unrelated to your current work, after committing a fix for a known flaky test, or to check flaky test status.
+allowed-tools: Read, Write, Bash, Glob
+---
+
+# Update Flaky Tests
+
+Track and manage flaky E2E test observations over time. This skill helps systematically log test failures that are unrelated to the current work, preserving error artifacts for later analysis.
+
+## STEP 1: Load Database
+
+Read the flaky tests database from `.workspace/flaky-tests/flaky-tests.json`.
+
+If the file or folder doesn't exist:
+1. Create the folder structure: `.workspace/flaky-tests/` and `.workspace/flaky-tests/artifacts/`
+2. Initialize the database using the schema at `/.claude/skills/update-flaky-tests/flaky-tests-schema.json`
+3. Create the main database file (`.workspace/flaky-tests/flaky-tests.json`):
+```json
+{
+ "lastUpdated": "",
+ "active": []
+}
+```
+4. Create an empty archive file (`.workspace/flaky-tests/flaky-tests-archived.json`):
+```json
+{
+ "lastUpdated": "",
+ "active": []
+}
+```
+
+## STEP 2: Auto-Maintenance
+
+Perform automatic maintenance on every run:
+
+1. Find tests in `active` array with status `fix-applied` where `lastSeen` is more than 7 days ago
+2. Move these tests to the archive file at `.workspace/flaky-tests/flaky-tests-archived.json`
+ - If archive file doesn't exist, create it with same structure: `{ "lastUpdated": "...", "active": [] }`
+ - Append tests to the archive's `active` array
+ - Remove tests from the main database's `active` array
+3. Report any auto-archived tests: "Auto-archived X tests that have been stable for 7+ days: [test names]"
+
+## STEP 3: Determine Context
+
+Assess what action is needed based on your current context:
+
+| Context | Mode |
+|---------|------|
+| Just ran E2E tests with failures | **Log mode** |
+| Just committed a fix for a known flaky test | **Fix mode** |
+| Neither / standalone check | **Status mode** |
+
+## STEP 4: Execute Based on Mode
+
+### Log Mode (after test failures)
+
+For each test failure you observed:
+
+1. **Classify the failure**:
+ - Is it related to your current work? Skip it (fix it as part of your task)
+ - Is it unrelated (flaky)? Log it
+
+2. **For unrelated failures, check if already tracked**:
+ - Search `active` array for matching `testFile` + `testName` + `stepName` + `browser`
+ - If found: increment `observationCount`, update `lastSeen`, add new observation
+ - If not found: create new entry with status `observed`
+
+3. **Preserve error artifacts**:
+ - Find the error-context.md in `application/*/WebApp/tests/test-results/test-artifacts/`
+ - Create timestamped folder: `.workspace/flaky-tests/artifacts/{timestamp}-{testFile}-{browser}-{stepName}/`
+ - Copy error-context.md (and screenshots if present) to this folder
+ - Store relative path in observation's `artifactPath` field
+
+4. **Auto-promote status**:
+ - If `observationCount` >= 2, change status from `observed` to `confirmed`
+
+**Observation fields to populate**:
+- `timestamp`: Current UTC timestamp (ISO 8601)
+- `branch`: Current git branch
+- `errorMessage`: The error message from the failure
+- `artifactPath`: Relative path to preserved artifacts
+- `observedBy`: Your agent type (qa-engineer, qa-reviewer, other)
+
+### Fix Mode (after committing a flaky test fix)
+
+1. Identify which flaky test was fixed (ask if unclear)
+2. Find the test in the `active` array
+3. Update the entry:
+ - Set `status` to `fix-applied`
+ - Populate the `fix` object:
+ - `appliedAt`: Current UTC timestamp
+ - `commitHash`: The commit hash of the fix
+ - `description`: Brief description of what was fixed
+ - `appliedBy`: Your agent type
+
+### Status Mode (standalone check)
+
+Read `/.claude/skills/update-flaky-tests/status-output-sample.md` first. Output status as a markdown table matching that format. Sort by Count descending. Omit Archived section if empty. End with legend line, nothing after.
+
+## STEP 5: Save Database
+
+1. Update `lastUpdated` to current UTC timestamp
+2. Write the updated database to `.workspace/flaky-tests/flaky-tests.json`
+3. Report changes made:
+ - "Added X new flaky test observations"
+ - "Updated X existing entries"
+ - "Marked X tests as fix-applied"
+ - "Auto-archived X resolved tests"
+
+## Key Rules
+
+**Only log tests you're confident are unrelated to your current work:**
+- If the test fails in code you're changing, fix it - don't log it as flaky
+- If you're unsure, err on the side of NOT logging
+
+**Preserve artifacts for comparison:**
+- What looks like the same flaky test might have subtle differences
+- Always copy the error-context.md when logging
+
+**Use local timestamps everywhere:**
+- All `timestamp`, `lastSeen`, `appliedAt`, `lastUpdated` fields use local time
+- Format: ISO 8601 without timezone suffix (e.g., `2026-01-14T14:30:00`)
+- **Get current local time**: Run `date +"%Y-%m-%dT%H:%M:%S"` - never guess the time
+
+## Reference Files
+
+- **Schema**: `/.claude/skills/update-flaky-tests/flaky-tests-schema.json`
+- **Sample database**: `/.claude/skills/update-flaky-tests/flaky-tests-sample.json`
+- **Sample archive**: `/.claude/skills/update-flaky-tests/flaky-tests-archived-sample.json`
+- **Sample status output**: `/.claude/skills/update-flaky-tests/status-output-sample.md`
+
+**Test entry structure** (unique key = testFile + testName + stepName + browser):
+```json
+{
+ "testFile": "account-management/WebApp/tests/e2e/user-management-flows.spec.ts",
+ "testName": "should handle user invitation and deletion workflow",
+ "stepName": "Delete user & verify confirmation dialog closes",
+ "browser": "Firefox",
+ "errorPattern": "confirmation dialog still visible after close",
+ "status": "confirmed",
+ "observations": [...],
+ "lastSeen": "2026-01-14T10:30:00",
+ "observationCount": 3,
+ "fix": null,
+ "notes": "Timing issue with dialog close animation"
+}
+```
+
+**Status lifecycle**:
+```
+observed (1 observation) -> confirmed (2+ observations) -> fix-applied -> archived (7+ days stable)
+```
diff --git a/.claude/skills/update-flaky-tests/flaky-tests-archived-sample.json b/.claude/skills/update-flaky-tests/flaky-tests-archived-sample.json
new file mode 100644
index 000000000..3c58e7007
--- /dev/null
+++ b/.claude/skills/update-flaky-tests/flaky-tests-archived-sample.json
@@ -0,0 +1,31 @@
+{
+ "lastUpdated": "2026-01-14T14:30:00",
+ "active": [
+ {
+ "testFile": "account-management/WebApp/tests/e2e/login-flows.spec.ts",
+ "testName": "should complete login with OTP verification",
+ "stepName": "Enter verification code & verify redirect to dashboard",
+ "browser": "Firefox",
+ "errorPattern": "keyboard.type drops characters",
+ "status": "fix-applied",
+ "observations": [
+ {
+ "timestamp": "2026-01-05T11:00:00",
+ "branch": "main",
+ "errorMessage": "keyboard.type failed to enter all characters",
+ "artifactPath": "2026-01-05T11-00-00Z-login-flows-enter-verification-code/",
+ "observedBy": "qa-engineer"
+ }
+ ],
+ "lastSeen": "2026-01-05T11:00:00",
+ "observationCount": 1,
+ "fix": {
+ "appliedAt": "2026-01-06T10:00:00",
+ "commitHash": "d6b6b25b5",
+ "description": "Fix OTP typing reliability in Firefox during parallel test execution",
+ "appliedBy": "qa-engineer"
+ },
+ "notes": "Stable for 7+ days after fix"
+ }
+ ]
+}
diff --git a/.claude/skills/update-flaky-tests/flaky-tests-sample.json b/.claude/skills/update-flaky-tests/flaky-tests-sample.json
new file mode 100644
index 000000000..864dfa2dd
--- /dev/null
+++ b/.claude/skills/update-flaky-tests/flaky-tests-sample.json
@@ -0,0 +1,80 @@
+{
+ "lastUpdated": "2026-01-14T14:30:00",
+ "active": [
+ {
+ "testFile": "account-management/WebApp/tests/e2e/login-flows.spec.ts",
+ "testName": "should complete login with OTP verification",
+ "stepName": "Click login button & wait for navigation",
+ "browser": "Firefox",
+ "errorPattern": "button click timeout",
+ "status": "observed",
+ "observations": [
+ {
+ "timestamp": "2026-01-14T10:00:00",
+ "branch": "pp-765-stabilize-flaky-e2e-tests",
+ "errorMessage": "Timeout waiting for button to be clickable after 5000ms",
+ "artifactPath": "2026-01-14T10-00-00-login-flows-click-login/",
+ "observedBy": "qa-engineer"
+ }
+ ],
+ "lastSeen": "2026-01-14T10:00:00",
+ "observationCount": 1,
+ "fix": null,
+ "notes": null
+ },
+ {
+ "testFile": "account-management/WebApp/tests/e2e/user-management-flows.spec.ts",
+ "testName": "should handle user invitation and deletion workflow",
+ "stepName": "Delete user & verify confirmation dialog closes",
+ "browser": "Firefox",
+ "errorPattern": "confirmation dialog still visible after close",
+ "status": "confirmed",
+ "observations": [
+ {
+ "timestamp": "2026-01-13T15:00:00",
+ "branch": "pp-765-stabilize-flaky-e2e-tests",
+ "errorMessage": "Expected element to not be visible but it was still present after 5000ms",
+ "artifactPath": "2026-01-13T15-00-00Z-user-management-flows-delete-user/",
+ "observedBy": "qa-engineer"
+ },
+ {
+ "timestamp": "2026-01-14T10:30:00",
+ "branch": "pp-765-stabilize-flaky-e2e-tests",
+ "errorMessage": "Expected element to not be visible but it was still present after 5000ms",
+ "artifactPath": "2026-01-14T10-30-00Z-user-management-flows-delete-user/",
+ "observedBy": "qa-engineer"
+ }
+ ],
+ "lastSeen": "2026-01-14T10:30:00",
+ "observationCount": 2,
+ "fix": null,
+ "notes": "Timing issue with dialog close animation in Firefox"
+ },
+ {
+ "testFile": "account-management/WebApp/tests/e2e/global-ui-flows.spec.ts",
+ "testName": "should handle theme switching and navigation",
+ "stepName": "Navigate to admin dashboard & verify welcome heading",
+ "browser": "WebKit",
+ "errorPattern": "heading not visible after navigation",
+ "status": "fix-applied",
+ "observations": [
+ {
+ "timestamp": "2026-01-12T09:15:00",
+ "branch": "main",
+ "errorMessage": "Timeout waiting for heading 'Welcome home' to be visible",
+ "artifactPath": "2026-01-12T09-15-00Z-global-ui-flows-navigate-dashboard/",
+ "observedBy": "qa-reviewer"
+ }
+ ],
+ "lastSeen": "2026-01-12T09:15:00",
+ "observationCount": 1,
+ "fix": {
+ "appliedAt": "2026-01-13T14:00:00",
+ "commitHash": "abc123def",
+ "description": "Added browser-specific auth state paths to fix cross-browser test isolation",
+ "appliedBy": "qa-engineer"
+ },
+ "notes": "WebKit ownerPage fixture was not properly isolated between tests"
+ }
+ ]
+}
diff --git a/.claude/skills/update-flaky-tests/flaky-tests-schema.json b/.claude/skills/update-flaky-tests/flaky-tests-schema.json
new file mode 100644
index 000000000..d661f01e6
--- /dev/null
+++ b/.claude/skills/update-flaky-tests/flaky-tests-schema.json
@@ -0,0 +1,135 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Flaky Tests Database",
+ "description": "Schema for tracking flaky E2E tests over time",
+ "type": "object",
+ "required": ["lastUpdated", "active"],
+ "properties": {
+ "lastUpdated": {
+ "type": "string",
+ "format": "date-time",
+ "description": "UTC timestamp of last database update"
+ },
+ "active": {
+ "type": "array",
+ "description": "Currently tracked flaky tests",
+ "items": {
+ "$ref": "#/$defs/flakyTest"
+ }
+ }
+ },
+ "$defs": {
+ "flakyTest": {
+ "type": "object",
+ "required": ["testFile", "testName", "stepName", "browser", "status", "lastSeen", "observationCount", "observations"],
+ "description": "Unique key is testFile + testName + stepName + browser. Each browser gets its own entry.",
+ "properties": {
+ "testFile": {
+ "type": "string",
+ "description": "Test file path including self-contained system (e.g., account-management/WebApp/tests/e2e/user-management-flows.spec.ts)"
+ },
+ "testName": {
+ "type": "string",
+ "description": "Full test name from the test description"
+ },
+ "stepName": {
+ "type": "string",
+ "description": "The step where the failure occurred"
+ },
+ "browser": {
+ "type": "string",
+ "enum": ["Chromium", "Firefox", "WebKit"],
+ "description": "Browser where the failure was observed"
+ },
+ "errorPattern": {
+ "type": "string",
+ "description": "Common error message pattern for this flaky test"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["observed", "confirmed", "fix-applied"],
+ "description": "Current status in the flaky test lifecycle"
+ },
+ "observations": {
+ "type": "array",
+ "description": "Individual failure observations",
+ "items": {
+ "$ref": "#/$defs/observation"
+ }
+ },
+ "lastSeen": {
+ "type": "string",
+ "format": "date-time",
+ "description": "UTC timestamp of most recent observation"
+ },
+ "observationCount": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Total number of times this test has been observed failing"
+ },
+ "fix": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "#/$defs/fix" }
+ ],
+ "description": "Fix information if a fix has been applied"
+ },
+ "notes": {
+ "type": "string",
+ "description": "Optional notes about the flaky test (suspected cause, workarounds, etc.)"
+ }
+ }
+ },
+ "observation": {
+ "type": "object",
+ "required": ["timestamp", "branch", "errorMessage", "observedBy"],
+ "properties": {
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "description": "UTC timestamp when observed"
+ },
+ "branch": {
+ "type": "string",
+ "description": "Git branch where failure was observed"
+ },
+ "errorMessage": {
+ "type": "string",
+ "description": "The actual error message from the test failure"
+ },
+ "artifactPath": {
+ "type": "string",
+ "description": "Relative path to preserved error artifacts in .workspace/flaky-tests/artifacts/"
+ },
+ "observedBy": {
+ "type": "string",
+ "enum": ["qa-engineer", "qa-reviewer", "other"],
+ "description": "Agent type that observed this failure"
+ }
+ }
+ },
+ "fix": {
+ "type": "object",
+ "required": ["appliedAt", "commitHash", "appliedBy"],
+ "properties": {
+ "appliedAt": {
+ "type": "string",
+ "format": "date-time",
+ "description": "UTC timestamp when fix was applied"
+ },
+ "commitHash": {
+ "type": "string",
+ "description": "Git commit hash containing the fix"
+ },
+ "description": {
+ "type": "string",
+ "description": "Brief description of what was fixed"
+ },
+ "appliedBy": {
+ "type": "string",
+ "description": "Agent type that applied the fix"
+ }
+ }
+ }
+ }
+}
diff --git a/.claude/skills/update-flaky-tests/status-output-sample.md b/.claude/skills/update-flaky-tests/status-output-sample.md
new file mode 100644
index 000000000..398aed1af
--- /dev/null
+++ b/.claude/skills/update-flaky-tests/status-output-sample.md
@@ -0,0 +1,19 @@
+# Flaky Test Tracker
+
+## Active (3)
+
+| Status | Test | Browser | Count | Last Seen |
+|--------|------|---------|-------|-----------|
+| 🟡 | user-management-flows.spec.ts | Firefox | 2 | Jan 14 10:30 |
+| 🔴 | login-flows.spec.ts | Firefox | 1 | Jan 14 10:00 |
+| 🟢 | global-ui-flows.spec.ts | WebKit | 1 | Jan 12 09:15 |
+
+## Archived (1)
+
+| Test | Browser | Fixed | Stable Since |
+|------|---------|-------|--------------|
+| login-flows.spec.ts | Firefox | Jan 6 | Jan 13 |
+
+---
+
+🔴 Observed · 🟡 Confirmed · 🟢 Fix Applied
diff --git a/.cursor/rules/frontend/frontend.mdc b/.cursor/rules/frontend/frontend.mdc
index e9647bc8f..e756e8c65 100644
--- a/.cursor/rules/frontend/frontend.mdc
+++ b/.cursor/rules/frontend/frontend.mdc
@@ -74,6 +74,7 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
- **Errors are handled globally**—`shared-webapp/infrastructure/http/errorHandler.ts` automatically shows toast notifications with the server's error message (don't manually show toasts for errors)
- **Validation errors**: Pass to forms via `validationErrors={mutation.error?.errors}`
- **`onError` is for UI cleanup only** (resetting loading states, closing dialogs), not for showing errors
+ - **Toast notifications**: Show success toasts in mutation `onSuccess` callbacks, not in `useEffect` watching `isSuccess` (avoids React effect scheduling delays)
4. Responsive design utilities:
- Use `useViewportResize()` hook to detect mobile viewport (returns `true` when mobile)
@@ -92,16 +93,21 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
- `z-[200]`: Mobile full-screen menus
- Note: Dropdowns, tooltips, and popovers use React Aria's overlay system which manages stacking relative to their context
-6. Always follow these steps when implementing changes:
+6. DirtyModal close handlers:
+ - **X button**: Use Dialog's `close` from render prop (shows unsaved warning if dirty)
+ - **Cancel button**: Use `handleCancel` that clears state and closes immediately (bypasses warning)
+ - Always clear dirty state in `onSuccess` and `onCloseComplete`
+
+7. Always follow these steps when implementing changes:
- Consult relevant rule files and list which ones guided your implementation
- Search the codebase for similar code before implementing new code
- Reference existing implementations to maintain consistency
-7. Build and format your changes:
+8. Build and format your changes:
- After each minor change, use the **execute MCP tool** with `command: "build"` for frontend
- This ensures consistent code style across the codebase
-8. Verify your changes:
+9. Verify your changes:
- When a feature is complete, run these MCP tools for frontend in sequence: **build**, **format**, **inspect**
- **ALL inspect findings are blocking** - CI pipeline fails on any result marked "Issues found"
- Severity level (note/warning/error) is irrelevant - fix all findings before proceeding
@@ -111,39 +117,49 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
```tsx
// ✅ DO: Correct patterns
-export function UserPicker({ isOpen, isPending, onOpenChange }: UserPickerProps) {
+export function UserPicker({ isOpen, onOpenChange }: UserPickerProps) {
+ const [isFormDirty, setIsFormDirty] = useState(false);
const { data } = api.useQuery("get", "/api/account-management/users", { enabled: isOpen });
const activeUsers = (data?.users ?? []).filter((u) => u.isActive); // ✅ Compute derived values inline
- const handleChangeSelection = (keys: Selection) => { /* ... */ }; // ✅ handleVerbNoun pattern
+ const inviteMutation = api.useMutation("post", "/api/account-management/users/invite", {
+ onSuccess: () => { // ✅ Show toast in onSuccess (not useEffect)
+ setIsFormDirty(false);
+ toastQueue.add({ title: t`Success`, description: t`User invited`, variant: "success" });
+ onOpenChange(false);
+ }
+ });
+
+ const handleCloseComplete = () => setIsFormDirty(false);
+ const handleCancel = () => { setIsFormDirty(false); onOpenChange(false); }; // ✅ Clear state + close (bypasses warning)
return (
- // ✅ Prevent dismiss during pending
+
-
+
);
}
@@ -152,11 +168,19 @@ function BadUserDialog({ users, selectedId, isOpen, onClose }) {
const [filteredUsers, setFilteredUsers] = useState([]); // ❌ State for derived values
const [isAdmin, setIsAdmin] = useState(false); // ❌ Duplicate state that can be calculated
+ const inviteMutation = api.useMutation("post", "/api/users/invite");
+
useEffect(() => { // ❌ useEffect for calculations - compute inline instead
setFilteredUsers(users.filter(u => u.isActive));
setIsAdmin(users.some(u => u.id === selectedId && u.role === "admin")); // ❌ Hardcode strings - use API contract types
}, [users, selectedId]);
+ useEffect(() => { // ❌ useEffect watching isSuccess causes toast timing issues
+ if (inviteMutation.isSuccess) {
+ toastQueue.add({ title: "Success", variant: "success" });
+ }
+ }, [inviteMutation.isSuccess]);
+
const getDisplayName = useCallback((user) => { // ❌ Premature useCallback without performance need
return `${user.firstName} ${user.lastName}`;
}, []);
@@ -166,17 +190,25 @@ function BadUserDialog({ users, selectedId, isOpen, onClose }) {
return (
// ❌ Missing isDismissable={!isPending}
);
diff --git a/.cursor/rules/workflows/process/review-end-to-end-tests.mdc b/.cursor/rules/workflows/process/review-end-to-end-tests.mdc
index 39488c167..c8b0ad259 100644
--- a/.cursor/rules/workflows/process/review-end-to-end-tests.mdc
+++ b/.cursor/rules/workflows/process/review-end-to-end-tests.mdc
@@ -45,11 +45,12 @@ You are reviewing: **{{{title}}}**
{
"todos": [
{"content": "Read [feature] and [task] to understand requirements", "status": "pending", "activeForm": "Reading feature and task"},
- {"content": "Run e2e tests and verify ALL pass with zero tolerance", "status": "pending", "activeForm": "Running E2E tests"},
+ {"content": "Run feature-specific e2e tests", "status": "pending", "activeForm": "Running feature E2E tests"},
{"content": "Review test file structure and organization", "status": "pending", "activeForm": "Reviewing test structure"},
{"content": "Review each test step for correct patterns", "status": "pending", "activeForm": "Reviewing test steps"},
{"content": "Review test efficiency and speed", "status": "pending", "activeForm": "Reviewing test efficiency"},
{"content": "Make binary decision (approve or reject)", "status": "pending", "activeForm": "Making decision"},
+ {"content": "If approved, run full regression test suite", "status": "pending", "activeForm": "Running full regression tests"},
{"content": "If approved, commit changes", "status": "pending", "activeForm": "Committing if approved"},
{"content": "Update [task] status to [Completed] or [Active]", "status": "pending", "activeForm": "Updating task status"},
{"content": "MANDATORY: Call CompleteWork", "status": "pending", "activeForm": "Calling CompleteWork"}
@@ -78,13 +79,13 @@ You are reviewing: **{{{title}}}**
- Read [End-to-End Tests](mdc:.cursor/rules/end-to-end-tests/end-to-end-tests.mdc)
- Ensure engineer followed all patterns
-**STEP 2**: Run e2e tests and verify ALL pass with zero tolerance
+**STEP 2**: Run feature-specific e2e tests first
**If tests require backend changes, run the run tool first**:
- Use **run MCP tool** to restart server and run migrations
- The tool starts .NET Aspire at https://localhost:9000
-**Run E2E tests**:
+**Run feature-specific E2E tests**:
- Use **end-to-end MCP tool** to run tests: `end-to-end(searchTerms=["feature-name"])`
- **ALL tests MUST pass with ZERO failures to approve**
- **Verify ZERO console errors** during test execution
@@ -152,7 +153,15 @@ You are reviewing: **{{{title}}}**
**When rejecting:** Do full review first, then reject with ALL issues listed (avoid multiple rounds).
-**STEP 7**: If approved, commit changes
+**STEP 7**: If approved, run full regression test suite
+
+**Before committing, run all e2e tests to ensure no regressions:**
+- Use **end-to-end MCP tool** WITHOUT searchTerms: `end-to-end()`
+- This runs the complete test suite across all browsers
+- **ALL tests MUST pass with ZERO failures**
+- If ANY test fails: REJECT (do not commit)
+
+**STEP 8**: Commit changes
1. Stage test files: `git add ` for each test file
2. Commit: One line, imperative form, no description, no co-author
@@ -160,7 +169,7 @@ You are reviewing: **{{{title}}}**
Don't use `git add -A` or `git add .`
-**STEP 8**: Update [task] status to [Completed] or [Active]
+**STEP 9**: Update [task] status to [Completed] or [Active]
**If `featureId` is NOT "ad-hoc" (regular task from a feature):**
- If APPROVED: Update [task] status to [Completed].
@@ -169,7 +178,7 @@ Don't use `git add -A` or `git add .`
**If `featureId` is "ad-hoc" (ad-hoc work):**
- Skip [PRODUCT_MANAGEMENT_TOOL] status updates.
-**STEP 9**: Call CompleteWork
+**STEP 10**: Call CompleteWork
**Call MCP CompleteWork tool**:
- `mode`: "review"
diff --git a/.github/copilot/rules/frontend/frontend.md b/.github/copilot/rules/frontend/frontend.md
index ab62aa16e..d5ab5ec8c 100644
--- a/.github/copilot/rules/frontend/frontend.md
+++ b/.github/copilot/rules/frontend/frontend.md
@@ -69,6 +69,7 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
- **Errors are handled globally**—`shared-webapp/infrastructure/http/errorHandler.ts` automatically shows toast notifications with the server's error message (don't manually show toasts for errors)
- **Validation errors**: Pass to forms via `validationErrors={mutation.error?.errors}`
- **`onError` is for UI cleanup only** (resetting loading states, closing dialogs), not for showing errors
+ - **Toast notifications**: Show success toasts in mutation `onSuccess` callbacks, not in `useEffect` watching `isSuccess` (avoids React effect scheduling delays)
4. Responsive design utilities:
- Use `useViewportResize()` hook to detect mobile viewport (returns `true` when mobile)
@@ -87,16 +88,21 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
- `z-[200]`: Mobile full-screen menus
- Note: Dropdowns, tooltips, and popovers use React Aria's overlay system which manages stacking relative to their context
-6. Always follow these steps when implementing changes:
+6. DirtyModal close handlers:
+ - **X button**: Use Dialog's `close` from render prop (shows unsaved warning if dirty)
+ - **Cancel button**: Use `handleCancel` that clears state and closes immediately (bypasses warning)
+ - Always clear dirty state in `onSuccess` and `onCloseComplete`
+
+7. Always follow these steps when implementing changes:
- Consult relevant rule files and list which ones guided your implementation
- Search the codebase for similar code before implementing new code
- Reference existing implementations to maintain consistency
-7. Build and format your changes:
+8. Build and format your changes:
- After each minor change, use the **execute MCP tool** with `command: "build"` for frontend
- This ensures consistent code style across the codebase
-8. Verify your changes:
+9. Verify your changes:
- When a feature is complete, run these MCP tools for frontend in sequence: **build**, **format**, **inspect**
- **ALL inspect findings are blocking** - CI pipeline fails on any result marked "Issues found"
- Severity level (note/warning/error) is irrelevant - fix all findings before proceeding
@@ -106,39 +112,49 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
```tsx
// ✅ DO: Correct patterns
-export function UserPicker({ isOpen, isPending, onOpenChange }: UserPickerProps) {
+export function UserPicker({ isOpen, onOpenChange }: UserPickerProps) {
+ const [isFormDirty, setIsFormDirty] = useState(false);
const { data } = api.useQuery("get", "/api/account-management/users", { enabled: isOpen });
const activeUsers = (data?.users ?? []).filter((u) => u.isActive); // ✅ Compute derived values inline
- const handleChangeSelection = (keys: Selection) => { /* ... */ }; // ✅ handleVerbNoun pattern
+ const inviteMutation = api.useMutation("post", "/api/account-management/users/invite", {
+ onSuccess: () => { // ✅ Show toast in onSuccess (not useEffect)
+ setIsFormDirty(false);
+ toastQueue.add({ title: t`Success`, description: t`User invited`, variant: "success" });
+ onOpenChange(false);
+ }
+ });
+
+ const handleCloseComplete = () => setIsFormDirty(false);
+ const handleCancel = () => { setIsFormDirty(false); onOpenChange(false); }; // ✅ Clear state + close (bypasses warning)
return (
- // ✅ Prevent dismiss during pending
+
-
+
);
}
@@ -147,11 +163,19 @@ function BadUserDialog({ users, selectedId, isOpen, onClose }) {
const [filteredUsers, setFilteredUsers] = useState([]); // ❌ State for derived values
const [isAdmin, setIsAdmin] = useState(false); // ❌ Duplicate state that can be calculated
+ const inviteMutation = api.useMutation("post", "/api/users/invite");
+
useEffect(() => { // ❌ useEffect for calculations - compute inline instead
setFilteredUsers(users.filter(u => u.isActive));
setIsAdmin(users.some(u => u.id === selectedId && u.role === "admin")); // ❌ Hardcode strings - use API contract types
}, [users, selectedId]);
+ useEffect(() => { // ❌ useEffect watching isSuccess causes toast timing issues
+ if (inviteMutation.isSuccess) {
+ toastQueue.add({ title: "Success", variant: "success" });
+ }
+ }, [inviteMutation.isSuccess]);
+
const getDisplayName = useCallback((user) => { // ❌ Premature useCallback without performance need
return `${user.firstName} ${user.lastName}`;
}, []);
@@ -161,17 +185,25 @@ function BadUserDialog({ users, selectedId, isOpen, onClose }) {
return (
// ❌ Missing isDismissable={!isPending}
);
diff --git a/.github/copilot/workflows/process/review-end-to-end-tests.md b/.github/copilot/workflows/process/review-end-to-end-tests.md
index 6dd992e7a..08c43e503 100644
--- a/.github/copilot/workflows/process/review-end-to-end-tests.md
+++ b/.github/copilot/workflows/process/review-end-to-end-tests.md
@@ -40,11 +40,12 @@ You are reviewing: **{{{title}}}**
{
"todos": [
{"content": "Read [feature] and [task] to understand requirements", "status": "pending", "activeForm": "Reading feature and task"},
- {"content": "Run e2e tests and verify ALL pass with zero tolerance", "status": "pending", "activeForm": "Running E2E tests"},
+ {"content": "Run feature-specific e2e tests", "status": "pending", "activeForm": "Running feature E2E tests"},
{"content": "Review test file structure and organization", "status": "pending", "activeForm": "Reviewing test structure"},
{"content": "Review each test step for correct patterns", "status": "pending", "activeForm": "Reviewing test steps"},
{"content": "Review test efficiency and speed", "status": "pending", "activeForm": "Reviewing test efficiency"},
{"content": "Make binary decision (approve or reject)", "status": "pending", "activeForm": "Making decision"},
+ {"content": "If approved, run full regression test suite", "status": "pending", "activeForm": "Running full regression tests"},
{"content": "If approved, commit changes", "status": "pending", "activeForm": "Committing if approved"},
{"content": "Update [task] status to [Completed] or [Active]", "status": "pending", "activeForm": "Updating task status"},
{"content": "MANDATORY: Call CompleteWork", "status": "pending", "activeForm": "Calling CompleteWork"}
@@ -73,13 +74,13 @@ You are reviewing: **{{{title}}}**
- Read [End-to-End Tests](/.github/copilot/rules/end-to-end-tests/end-to-end-tests.md)
- Ensure engineer followed all patterns
-**STEP 2**: Run e2e tests and verify ALL pass with zero tolerance
+**STEP 2**: Run feature-specific e2e tests first
**If tests require backend changes, run the run tool first**:
- Use **run MCP tool** to restart server and run migrations
- The tool starts .NET Aspire at https://localhost:9000
-**Run E2E tests**:
+**Run feature-specific E2E tests**:
- Use **end-to-end MCP tool** to run tests: `end-to-end(searchTerms=["feature-name"])`
- **ALL tests MUST pass with ZERO failures to approve**
- **Verify ZERO console errors** during test execution
@@ -147,7 +148,15 @@ You are reviewing: **{{{title}}}**
**When rejecting:** Do full review first, then reject with ALL issues listed (avoid multiple rounds).
-**STEP 7**: If approved, commit changes
+**STEP 7**: If approved, run full regression test suite
+
+**Before committing, run all e2e tests to ensure no regressions:**
+- Use **end-to-end MCP tool** WITHOUT searchTerms: `end-to-end()`
+- This runs the complete test suite across all browsers
+- **ALL tests MUST pass with ZERO failures**
+- If ANY test fails: REJECT (do not commit)
+
+**STEP 8**: Commit changes
1. Stage test files: `git add ` for each test file
2. Commit: One line, imperative form, no description, no co-author
@@ -155,7 +164,7 @@ You are reviewing: **{{{title}}}**
Don't use `git add -A` or `git add .`
-**STEP 8**: Update [task] status to [Completed] or [Active]
+**STEP 9**: Update [task] status to [Completed] or [Active]
**If `featureId` is NOT "ad-hoc" (regular task from a feature):**
- If APPROVED: Update [task] status to [Completed].
@@ -164,7 +173,7 @@ Don't use `git add -A` or `git add .`
**If `featureId` is "ad-hoc" (ad-hoc work):**
- Skip [PRODUCT_MANAGEMENT_TOOL] status updates.
-**STEP 9**: Call CompleteWork
+**STEP 10**: Call CompleteWork
**Call MCP CompleteWork tool**:
- `mode`: "review"
diff --git a/.windsurf/rules/frontend/frontend.md b/.windsurf/rules/frontend/frontend.md
index 766ef2ff0..5b9fa50e0 100644
--- a/.windsurf/rules/frontend/frontend.md
+++ b/.windsurf/rules/frontend/frontend.md
@@ -75,6 +75,7 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
- **Errors are handled globally**—`shared-webapp/infrastructure/http/errorHandler.ts` automatically shows toast notifications with the server's error message (don't manually show toasts for errors)
- **Validation errors**: Pass to forms via `validationErrors={mutation.error?.errors}`
- **`onError` is for UI cleanup only** (resetting loading states, closing dialogs), not for showing errors
+ - **Toast notifications**: Show success toasts in mutation `onSuccess` callbacks, not in `useEffect` watching `isSuccess` (avoids React effect scheduling delays)
4. Responsive design utilities:
- Use `useViewportResize()` hook to detect mobile viewport (returns `true` when mobile)
@@ -93,16 +94,21 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
- `z-[200]`: Mobile full-screen menus
- Note: Dropdowns, tooltips, and popovers use React Aria's overlay system which manages stacking relative to their context
-6. Always follow these steps when implementing changes:
+6. DirtyModal close handlers:
+ - **X button**: Use Dialog's `close` from render prop (shows unsaved warning if dirty)
+ - **Cancel button**: Use `handleCancel` that clears state and closes immediately (bypasses warning)
+ - Always clear dirty state in `onSuccess` and `onCloseComplete`
+
+7. Always follow these steps when implementing changes:
- Consult relevant rule files and list which ones guided your implementation
- Search the codebase for similar code before implementing new code
- Reference existing implementations to maintain consistency
-7. Build and format your changes:
+8. Build and format your changes:
- After each minor change, use the **execute MCP tool** with `command: "build"` for frontend
- This ensures consistent code style across the codebase
-8. Verify your changes:
+9. Verify your changes:
- When a feature is complete, run these MCP tools for frontend in sequence: **build**, **format**, **inspect**
- **ALL inspect findings are blocking** - CI pipeline fails on any result marked "Issues found"
- Severity level (note/warning/error) is irrelevant - fix all findings before proceeding
@@ -112,39 +118,49 @@ Use browser MCP tools to test at `https://localhost:9000`. Use `UNLOCK` as OTP v
```tsx
// ✅ DO: Correct patterns
-export function UserPicker({ isOpen, isPending, onOpenChange }: UserPickerProps) {
+export function UserPicker({ isOpen, onOpenChange }: UserPickerProps) {
+ const [isFormDirty, setIsFormDirty] = useState(false);
const { data } = api.useQuery("get", "/api/account-management/users", { enabled: isOpen });
const activeUsers = (data?.users ?? []).filter((u) => u.isActive); // ✅ Compute derived values inline
- const handleChangeSelection = (keys: Selection) => { /* ... */ }; // ✅ handleVerbNoun pattern
+ const inviteMutation = api.useMutation("post", "/api/account-management/users/invite", {
+ onSuccess: () => { // ✅ Show toast in onSuccess (not useEffect)
+ setIsFormDirty(false);
+ toastQueue.add({ title: t`Success`, description: t`User invited`, variant: "success" });
+ onOpenChange(false);
+ }
+ });
+
+ const handleCloseComplete = () => setIsFormDirty(false);
+ const handleCancel = () => { setIsFormDirty(false); onOpenChange(false); }; // ✅ Clear state + close (bypasses warning)
return (
- // ✅ Prevent dismiss during pending
+
-
+
);
}
@@ -153,11 +169,19 @@ function BadUserDialog({ users, selectedId, isOpen, onClose }) {
const [filteredUsers, setFilteredUsers] = useState([]); // ❌ State for derived values
const [isAdmin, setIsAdmin] = useState(false); // ❌ Duplicate state that can be calculated
+ const inviteMutation = api.useMutation("post", "/api/users/invite");
+
useEffect(() => { // ❌ useEffect for calculations - compute inline instead
setFilteredUsers(users.filter(u => u.isActive));
setIsAdmin(users.some(u => u.id === selectedId && u.role === "admin")); // ❌ Hardcode strings - use API contract types
}, [users, selectedId]);
+ useEffect(() => { // ❌ useEffect watching isSuccess causes toast timing issues
+ if (inviteMutation.isSuccess) {
+ toastQueue.add({ title: "Success", variant: "success" });
+ }
+ }, [inviteMutation.isSuccess]);
+
const getDisplayName = useCallback((user) => { // ❌ Premature useCallback without performance need
return `${user.firstName} ${user.lastName}`;
}, []);
@@ -167,17 +191,25 @@ function BadUserDialog({ users, selectedId, isOpen, onClose }) {
return (
// ❌ Missing isDismissable={!isPending}
);
diff --git a/.windsurf/workflows/process/review-end-to-end-tests.md b/.windsurf/workflows/process/review-end-to-end-tests.md
index 49954bfb5..7b08250fc 100644
--- a/.windsurf/workflows/process/review-end-to-end-tests.md
+++ b/.windsurf/workflows/process/review-end-to-end-tests.md
@@ -45,11 +45,12 @@ You are reviewing: **{{{title}}}**
{
"todos": [
{"content": "Read [feature] and [task] to understand requirements", "status": "pending", "activeForm": "Reading feature and task"},
- {"content": "Run e2e tests and verify ALL pass with zero tolerance", "status": "pending", "activeForm": "Running E2E tests"},
+ {"content": "Run feature-specific e2e tests", "status": "pending", "activeForm": "Running feature E2E tests"},
{"content": "Review test file structure and organization", "status": "pending", "activeForm": "Reviewing test structure"},
{"content": "Review each test step for correct patterns", "status": "pending", "activeForm": "Reviewing test steps"},
{"content": "Review test efficiency and speed", "status": "pending", "activeForm": "Reviewing test efficiency"},
{"content": "Make binary decision (approve or reject)", "status": "pending", "activeForm": "Making decision"},
+ {"content": "If approved, run full regression test suite", "status": "pending", "activeForm": "Running full regression tests"},
{"content": "If approved, commit changes", "status": "pending", "activeForm": "Committing if approved"},
{"content": "Update [task] status to [Completed] or [Active]", "status": "pending", "activeForm": "Updating task status"},
{"content": "MANDATORY: Call CompleteWork", "status": "pending", "activeForm": "Calling CompleteWork"}
@@ -78,13 +79,13 @@ You are reviewing: **{{{title}}}**
- Read [End-to-End Tests](/.windsurf/rules/end-to-end-tests/end-to-end-tests.md)
- Ensure engineer followed all patterns
-**STEP 2**: Run e2e tests and verify ALL pass with zero tolerance
+**STEP 2**: Run feature-specific e2e tests first
**If tests require backend changes, run the run tool first**:
- Use **run MCP tool** to restart server and run migrations
- The tool starts .NET Aspire at https://localhost:9000
-**Run E2E tests**:
+**Run feature-specific E2E tests**:
- Use **end-to-end MCP tool** to run tests: `end-to-end(searchTerms=["feature-name"])`
- **ALL tests MUST pass with ZERO failures to approve**
- **Verify ZERO console errors** during test execution
@@ -152,7 +153,15 @@ You are reviewing: **{{{title}}}**
**When rejecting:** Do full review first, then reject with ALL issues listed (avoid multiple rounds).
-**STEP 7**: If approved, commit changes
+**STEP 7**: If approved, run full regression test suite
+
+**Before committing, run all e2e tests to ensure no regressions:**
+- Use **end-to-end MCP tool** WITHOUT searchTerms: `end-to-end()`
+- This runs the complete test suite across all browsers
+- **ALL tests MUST pass with ZERO failures**
+- If ANY test fails: REJECT (do not commit)
+
+**STEP 8**: Commit changes
1. Stage test files: `git add ` for each test file
2. Commit: One line, imperative form, no description, no co-author
@@ -160,7 +169,7 @@ You are reviewing: **{{{title}}}**
Don't use `git add -A` or `git add .`
-**STEP 8**: Update [task] status to [Completed] or [Active]
+**STEP 9**: Update [task] status to [Completed] or [Active]
**If `featureId` is NOT "ad-hoc" (regular task from a feature):**
- If APPROVED: Update [task] status to [Completed].
@@ -169,7 +178,7 @@ Don't use `git add -A` or `git add .`
**If `featureId` is "ad-hoc" (ad-hoc work):**
- Skip [PRODUCT_MANAGEMENT_TOOL] status updates.
-**STEP 9**: Call CompleteWork
+**STEP 10**: Call CompleteWork
**Call MCP CompleteWork tool**:
- `mode`: "review"
diff --git a/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx b/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx
index 09e8d6538..223d319dd 100644
--- a/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx
+++ b/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx
@@ -60,7 +60,8 @@ export default function UserProfileModal({ isOpen, onOpenChange }: Readonly {
+ const handleCancel = () => {
+ handleCloseComplete();
onOpenChange(false);
};
@@ -92,11 +93,8 @@ export default function UserProfileModal({ isOpen, onOpenChange }: Readonly {
- if (saveMutation.isSuccess) {
+ },
+ onSuccess: () => {
toastQueue.add({
title: t`Success`,
description: t`Profile updated successfully`,
@@ -104,7 +102,7 @@ export default function UserProfileModal({ isOpen, onOpenChange }: Readonly {
if (files?.[0]) {
@@ -152,130 +150,134 @@ export default function UserProfileModal({ isOpen, onOpenChange }: Readonly
-
- Update your profile picture and personal details here.}>
-
- User profile
-
-
+ {({ close }) => (
+ <>
+
+ Update your profile picture and personal details here.}>
+
+ User profile
+
+
-
+
+
+
+
+
+ >
+ )}
)}
diff --git a/application/account-management/WebApp/routes/admin/account/index.tsx b/application/account-management/WebApp/routes/admin/account/index.tsx
index 72529ee96..26e22da4d 100644
--- a/application/account-management/WebApp/routes/admin/account/index.tsx
+++ b/application/account-management/WebApp/routes/admin/account/index.tsx
@@ -251,7 +251,17 @@ export function AccountSettings() {
refetch: refetchTenant
} = api.useQuery("get", "/api/account-management/tenants/current");
const { data: currentUser, isLoading: userLoading } = api.useQuery("get", "/api/account-management/users/me");
- const updateCurrentTenantMutation = api.useMutation("put", "/api/account-management/tenants/current");
+ const updateCurrentTenantMutation = api.useMutation("put", "/api/account-management/tenants/current", {
+ onSuccess: () => {
+ setIsFormDirty(false);
+ toastQueue.add({
+ title: t`Success`,
+ description: t`Account name updated successfully`,
+ variant: "success"
+ });
+ refetchTenant();
+ }
+ });
const updateTenantLogoMutation = api.useMutation("post", "/api/account-management/tenants/current/update-logo");
const removeTenantLogoMutation = api.useMutation("delete", "/api/account-management/tenants/current/remove-logo");
@@ -269,18 +279,6 @@ export function AccountSettings() {
hasUnsavedChanges: isFormDirty && isOwner
});
- useEffect(() => {
- if (updateCurrentTenantMutation.isSuccess) {
- setIsFormDirty(false);
- toastQueue.add({
- title: t`Success`,
- description: t`Account name updated successfully`,
- variant: "success"
- });
- refetchTenant();
- }
- }, [updateCurrentTenantMutation.isSuccess, refetchTenant]);
-
// Dispatch event to notify components about tenant updates
useEffect(() => {
if (
diff --git a/application/account-management/WebApp/routes/admin/users/-components/ChangeUserRoleDialog.tsx b/application/account-management/WebApp/routes/admin/users/-components/ChangeUserRoleDialog.tsx
index 9ecd9a559..938ffb5a1 100644
--- a/application/account-management/WebApp/routes/admin/users/-components/ChangeUserRoleDialog.tsx
+++ b/application/account-management/WebApp/routes/admin/users/-components/ChangeUserRoleDialog.tsx
@@ -53,7 +53,8 @@ export function ChangeUserRoleDialog({ user, isOpen, onOpenChange }: Readonly {
+ const handleCancel = () => {
+ setSelectedRole(null);
onOpenChange(false);
};
@@ -83,12 +84,9 @@ export function ChangeUserRoleDialog({ user, isOpen, onOpenChange }: Readonly