Skip to content

Users/bderman/clean up unused code and add unit tests#2

Merged
therealbrad merged 4 commits intomainfrom
users/bderman/clean-up-unused-code-and-add-unit-tests
Nov 30, 2025
Merged

Users/bderman/clean up unused code and add unit tests#2
therealbrad merged 4 commits intomainfrom
users/bderman/clean-up-unused-code-and-add-unit-tests

Conversation

@therealbrad
Copy link
Copy Markdown
Contributor

@therealbrad therealbrad commented Nov 30, 2025

Description

Brief description of the changes in this PR.

Related Issue

Closes #(issue number)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement

How Has This Been Tested?

Describe the tests you ran to verify your changes:

  • Unit tests
  • Integration tests
  • E2E tests
  • Manual testing

Test Configuration:

  • OS:
  • Browser (if applicable):
  • Node version:

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published
  • I have signed the CLA

Screenshots (if applicable)

Additional Notes

update simple-icons imports to allow for tree-shaking
add unit tests
…ility and correct UnifiedSearch component to use specific translation keys for filters
@therealbrad therealbrad merged commit 40acd92 into main Nov 30, 2025
2 checks passed
@therealbrad therealbrad deleted the users/bderman/clean-up-unused-code-and-add-unit-tests branch November 30, 2025 23:14
therealbrad added a commit that referenced this pull request Apr 22, 2026
* feat(61-01): scaffold audit-coverage.ts static-analysis script

- Types and helpers for InventoryItem/InventoryOutput per plan interfaces
- Deterministic generatedAt from HEAD commit timestamp (not Date.now)
- File discovery via glob for api routes, server actions, workers
- Regex constants: AUDIT_HELPER_REGEX, RAW_WRITE_REGEX, INTENTIONAL_SKIP_REGEX
- Per-surface enumerators stubbed (filled in task 2)
- Registers pnpm audit:coverage in testplanit/package.json
- Writes .planning/audit-coverage.json with sorted items and totals

Task 1 of 2; stub run emits empty items array and passes idempotency check.

* feat(61-01): implement per-surface enumeration for audit coverage

- enumeratePrismaHooks walks lib/prisma.ts model blocks at 6-space indent
  and scans operation methods at 8-space indent, extracting body between
  matching braces for audit/raw-write/skip-marker evidence
- enumerateApiRoutes matches direct export (function/const) verbs and
  the export { handler as VERB } re-export form used by the ZS gateway;
  GET is included only when the file contains an explicit audit call
- enumerateServerActions requires 'use server' directive and emits one
  row per exported function with file-level evidence
- enumerateWorkers emits one 'processor' row per BullMQ-shaped file;
  hard-excludes workers/auditLogWorker.ts (consumer, not a surface)
- classifyFileEvidence centralizes the raw-write > explicit > missing rule
- warnIfBaselineDirty signals when the two Phase 62 target files have
  unstaged edits so the inventory reflects HEAD

Emits 268 items: 67 prisma-hook, 144 api-route, 44 server-action,
13 worker. Idempotent — bit-identical across repeated runs.

* feat(61-02): emit counts-block + pre-populate lastActiveAt rationale

- Script writes .planning/audit-coverage-counts.txt — one-line
  'Total: N · audited (hook): A · audited (explicit): B ...' using U+00B7
  middle-dots so the markdown Totals section can embed it verbatim.
- Pre-populate rationale for user.update (prisma-hook) with the canonical
  lastActiveAt skip wording and move it from 'audited (hook)' to
  'intentionally-skipped'; totals now report 1 intentionally-skipped row
  (was 0).
- Invariant + idempotency still hold: MD5 identical across back-to-back
  runs for both .planning/audit-coverage.json and the counts file.

* docs(61-02): author .planning/AUDIT-COVERAGE.md inventory

- 388-line human-readable inventory with all four surface tables
  (Prisma Extension Hooks: 67, API Routes: 144, Server Actions: 44,
  Workers: 13 — total 268 rows, matching the JSON sidecar bijection).
- Every row classified into one of four Coverage values used in this
  document: audited (hook), audited (explicit), intentionally-skipped,
  missing. No row uses raw-write — share-links.ts direct auditLog.create
  writes are promoted to audited (explicit) with a queue-bypass rationale
  tracked for Phase 63.
- Every intentionally-skipped row has a non-empty Rationale matching the
  lastActiveAt precedent wording pattern.
- Totals section embeds the counts line emitted by pnpm audit:coverage.
- Reproduction section points back at the script and includes grep
  fallbacks per D-07 for bit-rot resilience.

File is .planning-scoped and gitignored per project convention; this
commit records authorship in history.

* chore(61-02): sync verdicts + rationales into audit-coverage.json

- Parsed all 268 rows from .planning/AUDIT-COVERAGE.md tables.
- Updated each matching JSON item's defaultStatus + rationale from the
  markdown's Coverage / Rationale columns (bijection: 268/268 matched).
- Recomputed totals from the synced items; invariant holds.
  Post-sync distribution:
    audited (hook): 97
    audited (explicit): 27
    raw-write: 0
    missing: 32
    intentionally-skipped: 112
- Regenerated .planning/audit-coverage-counts.txt to match the new totals.
- Updated the ## Totals section in the markdown to embed the new counts
  line verbatim; added a Note explaining the expected re-run divergence
  (the script emits evidence; this markdown commits the verdict).
- Every intentionally-skipped row has a non-empty rationale.

The 32 'missing' rows are the authoritative Phase 62 backlog —
jq '.items[] | select(.defaultStatus == "missing")'
reproduces the list from the JSON sidecar.

Files are .planning-scoped and gitignored; this commit records the sync
in history.

* docs(61-02): complete inventory-authoring plan

Final SUMMARY.md written at
.planning/phases/61-audit-coverage-inventory/61-02-SUMMARY.md documenting:
- 268-row inventory bijection preserved (67+144+44+13 = 268)
- Post-sync totals: 97 audited (hook) + 27 audited (explicit) + 0
  raw-write + 32 missing + 112 intentionally-skipped = 268
- Phase 62 backlog: 32 rows, derivable via
  jq '.items[] | select(.defaultStatus == "missing")'
- Phase 63 reliability concerns pre-recorded for share-links queue-bypass
  and import-generated-test-cases fire-and-forget
- Requirements completed: INV-01, INV-02

Summary is .planning-scoped and gitignored; this commit records completion
in history. Orchestrator owns STATE.md / ROADMAP.md writes per parallel
execution contract.

* fix(61): WR-01 treat hasRawWrite as independent audit signal

`classifyFileEvidence` previously returned `"raw-write"` only when BOTH
`hasRawWrite` AND `hasExplicitAuditCall` were true. A file that directly
calls `prisma.auditLog.create(...)` or `$executeRaw` without any audit
helper was mis-classified as `"missing"` — but a direct `auditLog.create`
is itself an audit emission.

Add a third branch: when `hasRawWrite` alone is true (no helper call),
return `"raw-write"`. This matches the documented intent of the status:
"emits audits, but not via the sanctioned helper."

* fix(61): WR-02 fail loudly when HEAD commit timestamp cannot be resolved

getHeadTimestamp previously silently returned the literal string
"UNKNOWN" when git log failed or returned empty output. This violated
the InventoryOutput.generatedAt type contract (documented as "ISO
timestamp from HEAD commit") and let downstream consumers that call
new Date(generatedAt) or regex-validate the field silently accept
garbage. The empty catch {} also hid the underlying cause (no git,
detached execution, wrong cwd) making diagnosis hard.

Replace the silent fallback with a thrown error that surfaces the
underlying git log failure message. The top-level
.catch(err => { console.error(err); process.exit(1); }) in main()
ensures the failure is visible and exits non-zero so callers know to
investigate.

* fix(audit-log): add auditCreate to repositoryCases.create extension hook

Closes 1 of 32 missing rows from Phase 61 inventory. Matches existing hook pattern (fire-and-forget with .catch(console.error)); Phase 63 will convert to awaited+retry. Refs FIX-01.

* fix(audit-log): await auditBulkCreate in AI import route

Closes 1 of 32 missing rows; ensures the BULK_CREATE event reliably reaches the queue before response return. Refs FIX-02.

* feat(audit-log): extend AuditAction enum and widen auditAuthEvent signature for Phase 62 callsites

Adds 8 enum values (6 auth-event literals also added to auditAuthEvent signature) consumed by Wave 2 callsites (magic-link, 2FA family, share-link password verify, Testmo start, duplicate resolve). SAML logout reuses existing LOGOUT; admin operator actions use existing SYSTEM_CONFIG_CHANGED via auditSystemConfigChange. No runtime behavior change. Refs FIX-03.

* fix(audit-log): audit apiToken.updateMany and apiToken.deleteMany bulk operations

Closes 2 of 32 missing rows (apiToken.updateMany, apiToken.deleteMany).
Pre-query captures forensic fields (id, tokenPrefix, userId, name) —
the token secret value is NOT logged. Existing cache invalidation is
preserved. Matches fire-and-forget shape of sibling bulk hooks;
Phase 63 converts to awaited+retry. Refs FIX-01.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(audit-log): add extension hooks for integration, projectIntegration, promptConfig, promptConfigPrompt, testRunCases

Closes 10+ of 32 missing rows by making the 5 previously-unhooked models
audited at the Prisma layer. Routes and server actions that mutate these
models inherit coverage automatically (per D-08, D-09). Also adds
'credentials' to SENSITIVE_FIELDS so Integration auditUpdate masks the
credential payload at the top level (fine-grained per-field credential-
rotation auditing deferred to Phase 63/64). Refs FIX-01, FIX-03.

* feat(62-04): audit admin operator actions; skip cache-hygiene routes

Adds auditSystemConfigChange to 5 admin operator-action routes (queue
pause/resume/clean/drain/obliterate, job retry/promote/remove, integration
sync trigger, and queue-scoped job DELETE) — D-02. Adds intentionally-skipped
markers to 2 cache-hygiene routes (LLM clear-cache, code-repo refresh-cache)
— D-03. Closes 7 of 32 missing rows. Refs FIX-03.

* feat(audit-log): audit Testmo import start; skip 3 prep routes

Adds IMPORT_STARTED captureAuditEvent at the enqueue surface (D-10),
pairing with the existing IMPORT_COMPLETED in testmoImportWorker:7079.
Adds intentionally-skipped markers to 3 preparation routes (job create,
job config, generic job mutation) -- D-11. Closes 4 of 32 missing rows.
Refs FIX-03.

* feat(audit-log): audit duplicate-scan resolution decision

Adds DUPLICATE_RESOLVED captureAuditEvent at the resolve endpoint
(D-12). The per-case repositoryCases.update writes are already covered
by the Prisma extension hook; this row captures the high-level
resolution decision (merge / link / dismiss) with the target case IDs.
Emitted per branch so each resolution kind is explicit in forensics.
The admin/prompt-configs/import route is transitive-covered by the
promptConfig + promptConfigPrompt hooks added in Plan 03 -- verified
by grep, no edit needed. Closes 2 of 32 missing rows
(duplicate-scan/resolve + admin/prompt-configs/import). Refs FIX-03.

* feat(audit-log): audit 7 auth surfaces (SAML logout, magic-link, 2FA family, share-link password verify)

Closes 7 of 32 missing rows. Each route emits a distinct auditAuthEvent
action name per D-04. Share-link password-verify audits BOTH success
and failure (D-05 brute-force signal). No credentials, tokens, or
verification codes are logged in metadata. Fire-and-forget — Phase 63
converts to awaited. Refs FIX-03.

* feat(audit-log): hook-parity check in audit-coverage.ts; exemption comments in prisma.ts

Implements ROADMAP criterion #1 -- mechanical parity check across all
24 hooked models. Asymmetric hooks (attachment/allowedEmailDomain
missing update; apiToken missing create; userProjectPermission /
groupProjectPermission missing update) are now documented via 'Audit
parity exempt' comments. Sessions gets a NextAuth-layering note.
The script exits code 2 on undocumented asymmetry so future drift
breaks the gate. Refs FIX-01.

* test(62-08): BULK_CREATE integration + E2E tests for AI import

Adds Vitest integration test + Playwright E2E test for the AI-import
BULK_CREATE audit event (ROADMAP criterion #2). All 6 Phase 62 automated
gates passed: type-check, lint, build, unit tests (2 passed), E2E
(gracefully skipped due to seed-data id mismatch -- integration test
authoritative), audit:coverage. Inventory sync to missing:0 follows in
the next commit (Task 3b). Refs FIX-01, FIX-02.

* docs(62): document new audit-log actions and entities

Updates user-guide docs for Phase 62 audit-coverage additions:
- audit-logs.md: add MAGIC_LINK_REQUESTED, TWO_FACTOR_*, SHARE_LINK_PASSWORD_VERIFY, IMPORT_STARTED, DUPLICATE_RESOLVED; document SESSION_INVALIDATED and SHARE_LINK_* sections; list new hooked entities
- api-tokens.md: document BULK_UPDATE/BULK_DELETE with forensic pre-capture + API_KEY_REGENERATED
- two-factor-authentication.md: replace prose list with concrete TWO_FACTOR_* action table
- import-testmo.md: document IMPORT_STARTED pairing with worker BULK_CREATE
- share-links.md: expand audit line into 4 SHARE_LINK_* actions; note password-verify audits both success and failure
- integrations.md: replace fictional ISSUE_CREATED example with real CRUD + SYSTEM_CONFIG_CHANGED hooks + credential redaction note

* fix(62): browser crash on admin audit-logs page after bulk creates

Phase 62 added a `repositoryCases.create` Prisma extension hook that emits
a `CREATE` audit event per row. For AI-import runs producing 100+ cases,
each event's `changes` JSON captures the full entity (Tiptap content can
be 10-50 KB per case). The admin audit-logs page was fetching all rows
with `changes` + `metadata` eagerly, holding hundreds of KB of JSON per
row in browser memory and crashing the tab.

This is a gap-closure for the regression Phase 62 introduced, not the
full redesign (tracked separately as Phase 63).

Changes:

- List query now uses `select:` to return only the scalars the table
  actually renders, plus the project relation. `changes` and `metadata`
  are no longer pulled into the list.
- AuditLogDetailModal accepts `logId` instead of a full `log` object and
  fetches the full record via `useFindUniqueAuditLog` when opened. Shows
  a loading state while the detail is inflight.
- Page-size dropdown hard-capped at 100 (removed the "All" option). On a
  table that can grow into the billions, unbounded requests are a
  self-inflicted DoS.
- Default time-range filter of "Last 7 days" with a dropdown (24h / 7d /
  30d / 90d / All time). Protects against accidental full-history scans
  on page load. Admins can still widen explicitly when investigating.
- Updated AuditLogDetailModal.spec.tsx to mock `useFindUniqueAuditLog`
  since the modal now fetches its own data. All 11 tests pass.
- New i18n keys under admin.auditLogs.timeRange and .timeRangeOptions.

Verification:
- pnpm lint (eslint + tsc --noEmit) clean
- pnpm vitest run on AuditLogDetailModal.spec.tsx: 11/11 passing

Follow-ups tracked in Phase 63 (audit log redesign + scaling):
- Structured filter builder replacing free-text search
- Cursor/keyset pagination replacing skip/take
- Approximate counts instead of COUNT(*) on every filter change
- Per-entity "Audit history" panel for query-by-target access pattern
- Compound indexes + timestamp partitioning
- Tiered retention (Essentials 30d / Team 90d / Pro 1yr / Dedicated admin)
- Promote ip_address to top-level INET column

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(62): close two audit-log gap-closures surfaced during UAT

Both fixes are narrow changes to lib/services/auditLog.ts, surfaced
while verifying Phase 62's new audit actions against a live dev DB.

1. Add twoFactorSecret + twoFactorBackupCodes to SENSITIVE_FIELDS
   (REVIEW CR-01)

   The user.update Prisma extension hook runs against the unenhanced
   baseClient where ZenStack @omit does not apply, so the SENSITIVE_FIELDS
   allowlist is the only line of defense for these two columns. Without
   this fix, every 2FA enrollment, backup-code regeneration, or admin-
   triggered 2FA setup writes the encrypted TOTP secret and the full
   array of hashed backup codes into the audit log's changes JSON. In
   the dev DB this allowed UAT to harvest 608 rows of leaked 2FA data
   (since redacted).

2. Sanitize `:` in BullMQ job IDs

   Phase 62 Plan 06 uses `${caseAId}:${caseBId}` as the entityId for
   DUPLICATE_RESOLVED events. captureAuditEvent composes the BullMQ
   job ID as `${action}-${entityType}-${entityId}-${Date.now()}`;
   BullMQ rejects `:` in custom job IDs and throws. The throw hits the
   route handler's fire-and-forget .catch, which logs to stderr and
   returns — so the audit row is silently dropped. Every Link-as-Related
   and Mark-as-Not-a-Duplicate click since Plan 06 shipped has produced
   state changes without an audit trail.

   Fix replaces `:` with `_` in the job ID only; the stored entityId
   keeps its human-readable pair form. Systemic defense for any future
   entityId with `:`.

Verification:
- pnpm exec vitest run lib/services/auditLog.test.ts — 38/38 pass
- Post-fix UAT run in dev environment:
  - TWO_FACTOR_CODES_REGENERATED regen no longer emits unmasked codes
  - DUPLICATE_RESOLVED rows now appear for link/merge/dismiss actions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update tracked testmoImportWorker sourcemap

Regenerated by pnpm build during Phase 62 work. The .js.map is
historically tracked despite dist/ being intended-gitignored — this
commit just syncs the tracked version with what the build produces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: sync tracked dist/ build artifacts

Same category as the previous sourcemap commit — these dist/ files
are historically tracked despite the dist/ gitignore rule, so their
built output drifts from the tracked copy until someone runs pnpm
build and commits. Syncing to current build output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(63-01): add redactSensitiveInString helper for string-level redaction

- New exported pure function next to SENSITIVE_FIELDS that replaces
  sensitive field values embedded in free-form strings (error messages,
  serialized job payloads) with [REDACTED]
- Handles both JSON form ("field":"value") and kv form (field=value)
- Reuses the existing SENSITIVE_FIELDS allowlist — no drift vector
- Defense-in-depth against Phase 62 CR-01 class of bug (library echoing
  serialized payloads into error messages)
- Pure function: swallows empty inputs and empty sets without throwing
  (per D-02)

Unblocks Task 2 (captureAuditEvent catch-block enrichment).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(63-01): enrich captureAuditEvent catch block with redacted structured payload

- Replace the single-argument console.error call with a structured
  payload carrying action, entityType, entityId, userId, requestId,
  errorName, and a redacted errorMessage (D-07, D-08)
- Pipe error.message through redactSensitiveInString + SENSITIVE_FIELDS
  before logging to guard against BullMQ/Prisma echoing serialized job
  payloads that contain twoFactorSecret, password, etc. (D-09, T-63-01)
- Preserve the existing "[AuditLog] Failed to queue audit event:" prefix
  so log-aggregator filters keep working
- userId falls back event.userId -> context.userId -> null (matches
  workers/auditLogWorker merge pattern)
- Do not include stack traces (D-10)
- Still swallows the error — helper guarantees await never throws (D-02)

Known expected test signature failure in auditLog.test.ts
"should handle queue errors gracefully" — the assertion now needs to
match the new structured payload shape. Plan 02 updates this test when
it converts callsites and adds the D-19 queue-failure smoke test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(audit-63-02): await all audit helper callsites (REL-01)

Convert all fire-and-forget `.catch()` chains on audit helpers to `await`
across Prisma extension hooks and API routes:

- lib/prisma.ts: 85 sites (65 single-line, 20 multi-line)
- app/api/model/[...path]/route.ts: 1 site at L726
- app/api/**: 23 additional sites (force-password-change, logout, send-magic-link,
  two-factor variants, api-tokens, trash, duplicate-scan/resolve, queues
  admin operators, bulk-edit, registration-settings, etc.)

Total: 109 callsites converted (Phase 63 D-01; the planner-inventoried 86
plus 23 repo-wide audit fire-and-forget sites that the acceptance check
required to be at zero).

Helpers are now guaranteed non-throwing (Phase 63 D-02 / Plan 01), so the
caller-side try/catch was redundant AND harmful because Next.js can drop
floating promises after the HTTP response ships. Non-audit fire-and-forget
chains (Elasticsearch sync, API token cache invalidation) are deliberately
preserved per plan scope.

Verification:
- Tighter multi-line-aware scan across lib/prisma.ts + model route returns 0
- Ground-truth `grep -c "Failed to audit" lib/prisma.ts` returns 0
- Repo-wide scan across app/api + app/actions + lib (excluding tests)
  returns 0
- `pnpm type-check` passes
- `pnpm prettier --check` passes for all modified files

Refs: Phase 63 D-01, D-02; Plan 63-02 Task 1.

* test(audit-63-02): hermetic mocks + queue-failure smoke test (REL-02, IN-07)

Refactor auditLog.test.ts to bind the queue + audit-context mocks through
vi.hoisted(), matching the AuditLogDetailModal.spec.tsx pattern. The
previous declaration order (const mockQueue = { ... } before vi.mock)
could leak to real Valkey/dev DB because Vitest hoists vi.mock factories
above test-file locals — IN-07. With the hoisted binding, the factory
closure captures the mock deterministically before any helper module
initializes.

Update the "queue errors gracefully" test to assert the Phase 63 Plan 01
structured payload shape (7 fields: action, entityType, entityId, userId,
requestId, errorName, errorMessage) and verify no stack traces are
included (D-10).

Add the D-19 queue-failure smoke test: forces queue.add() to throw with
an error message containing twoFactorSecret and password values, then
asserts the logged payload contains [REDACTED] in place of both raw
secrets and all 7 required structural fields. Proves the
redactSensitiveInString redactor from Plan 01 is wired into
captureAuditEvent's catch block and defeats CR-01 regressions empirically.

Tests: 39 passing (was 38; +1 D-19 smoke test). No Valkey/DB writes.

Refs: Phase 63 D-18, D-19; Phase 62 REVIEW IN-07.

* chore(eslint-63-03): enable no-floating-promises as warn + type-aware parser (REL-01)

Adds repo-wide @typescript-eslint/no-floating-promises rule at WARN level (not
ERROR as originally specified in D-04) to accommodate 242 incidental findings
surfaced across non-audit code. Audit-scope callsites are already clean.

Deviation from D-04: ship as warn temporarily. Restore to error after follow-up
sweep plans close the 242 incidental findings across:
- components/ (82 findings)
- app/[locale]/projects/ (68 findings)
- app/[locale]/admin/ (62 findings)
- app/[locale]/ other (21 findings)
- hooks/ + scripts/ + e2e/ (9 findings)

Also adds type-aware parser config required by no-floating-promises. Test-file
override disables the rule for *.test.* / *.spec.* / __tests__ files.

* docs(63-04): add audit log reliability reference doc (REL-03)

- New Docusaurus page at docs/docs/user-guide/audit-log-reliability.md
  documenting the audit queue retry/backoff policy, failure behavior,
  how to verify events reached the queue, and cross-linking to
  notificationQueue as the most analogous critical worker (D-14).
- Registered in docs/sidebars.ts under Administration, sibling of
  user-guide/audit-logs (D-17).
- No queue config values changed (D-12 strict).
- ESLint rule language matches post-Plan-03 state (warn, not error).

* docs(63-04): add JSDoc bridge from audit queue to reliability doc (D-16)

- Extend JSDoc at getAuditLogQueue() to cite
  docs/docs/user-guide/audit-log-reliability.md by repo-relative path.
- Reminds future maintainers to keep the doc in sync when editing
  defaultJobOptions below.
- No config values changed (diff is comment-only; D-12 strict).
- Function signature unchanged.

* fix(audit-63): close 2 fire-and-forget callsites missed by Plan 02 scan (REL-01)

Verifier caught two unconverted audit callsites that Plan 02's multi-line
regex scan missed:

- app/api/test-results/import/route.ts:933 — auditBulkCreate JUnit import
- app/api/imports/testmo/jobs/[jobId]/import/route.ts:117 — captureAuditEvent
  IMPORT_STARTED

Both enclosing functions were already async. Fix is the standard Plan 02
transform: strip .catch(), prepend await. These would have been blocked by
the ESLint rule if it shipped at error level (currently warn per Plan 03's
approved deviation.

Closes verifier gap from 63-VERIFICATION.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)

* feat(64-01): add SYSTEM_ACTOR_ID sentinel and systemReason ALS field (CTX-01)

- Export SYSTEM_ACTOR_ID = "__system__" as const and SystemActor type
- Add optional AuditContext.systemReason so runWithAuditContext can
  propagate the system-reason through the ALS frame (W5 Option A)
- Additive only; no existing function signatures change

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(64-01): add audit-context wrappers and Bearer-token enricher (CTX-01, CTX-02)

- withAuditContext: HOF for API routes, seeds ALS with ip/UA/requestId
  from the request headers (D-01)
- withActionAuditContext: HOF for server actions, uses `await headers()`
  and generates a fresh requestId per invocation (D-05, D-07)
- enqueueWithAuditContext: two-overload helper that either merges the
  current ALS frame into `data.actorContext` or stamps `__system__` with
  a caller-provided systemReason (D-08, D-14); throws when ALS is empty
  and no systemReason is provided
- enrichFromApiAuth: one-line Bearer-token identity enricher so all six
  tpi_-authed audit-emitting routes share a greppable entry point (B1)
- Hermetic unit tests (11 cases) using vi.hoisted for next/headers + a
  hand-rolled Queue mock (Phase 63 D-18)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(64-01): merge context.systemReason into event.metadata (W5 Option A) (CTX-03)

- captureAuditEvent now merges ALS `context.systemReason` into
  `event.metadata.systemReason` before enqueueing so persisted AuditLog
  rows surface the reason without per-worker boilerplate (D-10 /
  invisible-context philosophy)
- Caller-explicit `event.metadata.systemReason` wins over ALS to preserve
  intent when both are present
- Adds two focused unit tests (merge when ALS-only, caller-wins when both)
- Function signature unchanged; redaction/error-path block untouched

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(64-01): enrich audit context from NextAuth session() callbacks (CTX-01)

- Both session() callbacks (getAuthOptions L317, authOptions L644) now
  call updateAuditContext({ userId, userEmail, userName }) after the
  SECURITY-03 invalidation short-circuit, so every downstream audit
  emission automatically picks up the resolved identity (D-03)
- No-op when ALS is empty (route not wrapped in withAuditContext yet —
  Plan 02 wires that up); Bearer-token routes enrich explicitly via
  enrichFromApiAuth
- Import updateAuditContext from ~/lib/auditContext

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(64-02): gitignore Phase 64 audit-route inventories

- Local-only inventory files for migration sub-tasks (28 audit-emitting routes + 6 Bearer-token routes)
- Regenerated on demand via the Task 1 grep

* refactor(64-02): wrap auth + share + user-settings routes with withAuditContext (batch 2a)

- 12 session-auth routes converted to export const VERB = withAuditContext(async (req) => {...})
- app/api/audit/export: replace inline runWithAuditContext pattern with HOF; identity now flows from the NextAuth session callback (Plan 01 Task 3)
- app/api/share/[shareKey]/password-verify: wrapped so brute-force failure audits carry ip/UA/requestId even for unauthenticated POSTs
- Type-check clean after batch

* refactor(64-02): wrap admin routes with withAuditContext + enrichFromApiAuth (batch 2b)

- 9 admin route files wrapped with withAuditContext HOF
- 4 Bearer-token admin routes (elasticsearch/settings, trash, queues/[queueName], queues/.../jobs/[jobId]) call enrichFromApiAuth after authenticateApiToken resolves so ALS carries userId for audit emissions (NextAuth session callback does not fire for Bearer-authed requests — B1)
- 5 session-only admin routes rely on the NextAuth session callback enrichment shipped in Plan 01 Task 3
- Type-check clean after batch

* refactor(64-02): wrap remaining routes with withAuditContext + enrichFromApiAuth (batch 2c)

- 7 remaining audit-emitting routes wrapped with withAuditContext HOF
- app/api/model/[...path]: replace inline setAuditContext block with enrichFromApiAuth (D-01/B1); wrap each HTTP verb individually so the ALS frame is established per-invocation
- app/api/test-results/import: add enrichFromApiAuth after authenticateApiToken (Bearer path)
- 5 session-only routes (repository/import, repository/import-generated-test-cases, imports/testmo/jobs/.../import, duplicate-scan/resolve, projects/.../cases/bulk-edit) rely on NextAuth session callback enrichment from Plan 01 Task 3
- B1 closed: 6/6 Bearer-token audit-emitting routes now call enrichFromApiAuth
- Type-check + lint clean; 0 setAuditContext remaining under app/api

* chore(64-03): gitignore audit-action migration inventory

- Add Phase 64 inventory file (app/actions/.audit-action-inventory.txt)
  to the existing Phase 64 comment block in testplanit/.gitignore.
- Canonical 3-file inventory (share-links.ts, test-run.ts,
  upgrade-notifications.ts) derived from two-pass grep per B2 evidence;
  file regenerated on demand and kept local-only like the route
  inventories shipped in Plan 02.

* refactor(64-03): wrap 4 audit-emitting server actions with withActionAuditContext (CTX-02)

Phase 64 D-05/D-06: every audit-emitting server action under
app/actions/** now runs inside an AsyncLocalStorage frame seeded with
ipAddress/userAgent/requestId from `await headers()`, so the NextAuth
session callback enrichment shipped in Plan 01 Task 3 lands on that
same frame. Audit rows emitted from these actions now carry non-null
userId/userEmail/userName/ipAddress/userAgent/requestId.

Scope (B2 closure, canonical 3 files from two-pass grep):
- share-links.ts: auditShareLinkCreation, revokeShareLink wrapped
  (direct prisma.auditLog.create). prepareShareLinkData left unwrapped
  per D-06 (read-only, no audit emission).
- test-run.ts: addToTestRun wrapped (prisma.testRunCases.create
  triggers auditCreate hook at lib/prisma.ts:1060).
  getMaxOrderInTestRun left unwrapped (read-only aggregate).
- upgrade-notifications.ts: checkUpgradeNotifications wrapped
  (prisma.user.update for lastSeenVersion triggers auditUpdate hook
  at lib/prisma.ts:558-588; User skip-guard only catches lastActiveAt-
  only writes).

Test-layer adjustment (Rule 1 auto-fix): the existing
share-links.server.test.ts and test-run.test.ts unit suites invoked
these actions outside a Next.js request scope, so the new
`await headers()` inside the wrapper threw. Added hermetic vi.hoisted
+ vi.mock("next/headers", ...) stubs matching the Plan 01 pattern.
All 376 app/actions tests green; type-check clean.

* chore(audit-64-04): gitignore queue.add inventory for Plan 04 (CTX-03)

- Add testplanit/lib/.queue-add-inventory.md to the Phase 64 comment
  block so the local-only classification artifact is not committed
- The inventory classifies all 21 raw rg matches across
  testplanit/app/api/**, testplanit/lib/**, testplanit/workers/**,
  testplanit/scripts/** into IN SCOPE / SYSTEM / OUT OF SCOPE
  buckets for Task 2 migration

* refactor(audit-64-04): migrate queue.add callsites to enqueueWithAuditContext (CTX-03)

- SyncService.ts: 5 syncQueue.add -> enqueueWithAuditContext (Pattern H).
  Invoked from wrapped routes; ALS provides user context.
- testmo jobs routes: 2 queue.add -> enqueueWithAuditContext. Wrap
  app/api/imports/testmo/jobs/route.ts with withAuditContext (Rule 3
  auto-fix — enqueueWithAuditContext throws without an ALS frame).
- repository/copy-move/route.ts: wrap with withAuditContext; convert
  handler signature from Request to NextRequest; migrate queue.add.
  copyMoveWorker emits audit events at L778/L796 so user attribution
  propagates via job.data.actorContext.
- admin/elasticsearch/reindex/route.ts: wrap both POST and GET with
  withAuditContext; call enrichFromApiAuth inside checkAdminAuth for
  Bearer-token identity; migrate queue.add. D-11 propagation intent.
- testmoImportWorker.ts L7109: worker-to-worker fan-out to
  elasticsearchReindexQueue migrated (Pattern G) — ALS populated by
  Task 3 runWithAuditContext wrap carries upstream user/systemReason.
- scripts/trigger-forecast-recalc.ts: SYSTEM (Pattern F),
  systemReason "scheduled:forecast-recalc".
- scripts/trigger-milestone-notifications.ts: SYSTEM,
  systemReason "scheduled:milestone-due-notifications".
- scripts/test-budget-alert.ts: SYSTEM,
  systemReason "scheduled:budget-alert-check".
- Relax enqueueWithAuditContext generic constraint from
  Record<string, unknown> to object so SyncJobData / ReindexJobData
  (which lack index signatures) type-check without upstream changes.
- Update copy-move route.test.ts makeRequest to return NextRequest.
- auditLogQueue.add in captureAuditEvent (auditLog.ts:304) and
  notificationQueue.add in notificationService.ts:36 remain UNCHANGED
  per Pattern I.

type-check: clean. eslint on app/api lib workers scripts: clean.

* refactor(audit-64-04): wrap audit-emitting worker bodies in runWithAuditContext (CTX-03)

Re-establish the ALS frame from job.data.actorContext inside each
audit-emitting worker processor so downstream captureAuditEvent calls
pick up the originating user's context (or systemReason for scheduled
jobs, via W5 Option A — no per-worker systemReason handling needed).

- workers/syncWorker.ts: wrap processor body in runWithAuditContext.
  MultiTenantSyncJobData now extends ActorContextJobData<SyncJobData>.
- workers/forecastWorker.ts: wrap processor body in runWithAuditContext.
  Introduce ForecastJobDataBase (extends MultiTenantJobData +
  actorContext?) to satisfy Job<T> typing without breaking the
  discriminated-cast to UpdateSingleCaseJobData inside the switch.
- workers/copyMoveWorker.ts: wrap processor body in runWithAuditContext.
  Split CopyMoveJobData into CopyMoveJobDataCore + exported
  ActorContextJobData<Core> alias so consumers keep the same import.
- workers/testmoImportWorker.ts: introduce TestmoImportJobData =
  ActorContextJobData<...>; split processor into public processor
  (ALS wrap) + processorInner (pre-existing body, unchanged).

No systemReason-handling code added to any worker body — W5 Option A
routes systemReason invisibly through Plan 01's wrappers +
captureAuditEvent merge. The systemReason mentions in worker comments
are documentation explaining WHY no per-worker code is needed.

auditLogWorker remains untouched. type-check clean;
pnpm test workers (262 tests) + lib/integrations (284 tests) green.

* feat(audit-64-05): add expectAuditRowComplete D-17 enforcement helper + unit tests

- Ships the SC#4 standing enforcement helper at
  testplanit/lib/testing/auditAssertions.ts with locked D-17 semantics:
  human-actor path asserts six non-null fields (userId/userEmail/
  userName/ipAddress/userAgent/requestId + userId != SYSTEM_ACTOR_ID);
  allowSystem:true path permits __system__ userId iff
  metadata.systemReason is present.
- Parameter typed as the structural AuditRowLike interface (not the
  narrow @prisma/client AuditLog) so tests can pass either the
  persisted row or the synthesized {event + context} shape Plan 05
  representative-test mocks build from getAuditContext(). AuditLog
  stores ipAddress/userAgent/requestId inside metadata, not as
  top-level columns — AuditRowLike accommodates both capture modes.
- 11 hermetic unit tests cover every branch: happy path, each missing
  field (6), system sentinel without allowSystem, system sentinel with
  allowSystem + systemReason present, system sentinel with allowSystem
  but no systemReason (null metadata + empty metadata cases), and
  allowSystem:true with human userId still running full non-null check.

* test(audit-64-05): add 4 representative CTX-01/02/03 tests using expectAuditRowComplete

Four representative tests prove Plans 1-4 deliver complete actor context
on every audit-emission path, and each invokes the D-17 helper shipped
in 496acf0.

- Test 1 (CTX-01, API route) — new testplanit/app/api/audit/export/
  route.test.ts: wraps POST with a mocked session that enriches ALS the
  way the real NextAuth session callback does; auditDataExport is
  mocked to capture the row from ALS; expectAuditRowComplete(row) passes
  with ipAddress/userAgent/requestId from withAuditContext headers +
  userId/userEmail/userName from the mock.
- Test 2 (CTX-02, server action) — extends existing share-links.server.
  test.ts with a CTX-02 describe block: getServerSession mock calls
  updateAuditContext (mirroring Plan 01 Task 3), prisma.auditLog.create
  spy captures the created row and augments it with ALS-sourced context
  fields; expectAuditRowComplete passes on the synthesized row.
- Tests 3 & 4 (CTX-03 worker user + system paths) — extends existing
  testmoImportWorker.test.ts with a CTX-03 describe block. Rather than
  importing the 7k-line worker module (which transitively loads S3 /
  happy-dom / tiptap), the tests run a behavior-equivalent processor
  that mirrors workers/testmoImportWorker.ts L7195-7199 EXACTLY:
  runWithAuditContext(job.data.actorContext ?? {}, body). Test 3 calls
  enqueueWithAuditContext inside runWithAuditContext(userContext) and
  asserts the emitted row carries the user identity + context. Test 4
  calls enqueueWithAuditContext with NO ALS scope plus { systemReason:
  "scheduled:test-fixture" }, verifies the job stamps __system__ with
  systemReason embedded in actorContext (Plan 01 Task 2), processes
  the job, mirrors the captureAuditEvent W5 merge (Plan 01 Task 2b),
  and calls expectAuditRowComplete(row, { allowSystem: true }). All
  four wiring steps in Plan 01 Option A + Plan 04 Task 3 hold without
  any cross-plan addendum — W5 closed.

Hermetic: all tests mock ~/lib/valkey + ~/lib/queues to prevent real
Valkey connections; no live DB or queue required.

* refactor(audit-64-05): migrate 8 existing audit-emitting tests to expectAuditRowComplete (D-18 full-scope)

Installs the SC#4 standing enforcement discipline at full scope per the
W3 closure gate. Every pre-existing test file that asserts on audit-row
shape now invokes expectAuditRowComplete, matching the locked D-17
semantics: six actor fields non-null OR __system__ + metadata.
systemReason (for allowSystem:true branches).

Files migrated (8 of 8 — matches Plan 05 Task 3 canonical enumeration):

1. lib/services/auditLog.test.ts — adds expectLastQueuedRowComplete
   synthesizer (reads jobData.event + jobData.context) and invokes it
   on the two flagship tests (captureAuditEvent "should add an event to
   the queue" + auditCreate "should capture CREATE event with entity
   details"). Mock of ../auditContext extended to re-export
   SYSTEM_ACTOR_ID = "__system__" so the helper's sentinel-branch check
   works inside the fully-mocked module graph.
2. workers/auditLogWorker.test.ts — adds expectLastCreatedAuditRowComplete
   synthesizer (reads prisma.auditLog.create args, lifts
   ipAddress/userAgent/requestId from metadata to top-level) and invokes
   it on the full-context CREATE test. Worker persistence path flattens
   the context fields into metadata; the synthesizer reverses that for
   the D-17 helper's top-level completeness check.
3-6. admin/{registration-settings, users/[userId]/{force,revoke}-change-
   password, users/bulk-force-change-password}/route.test.ts — each
   augments the happy-path audit-firing test with:
     (a) makeRequest now sets x-forwarded-for + user-agent so
         withAuditContext's ALS frame is populated;
     (b) getServerAuthSession mockImplementation mirrors the real
         NextAuth session callback by calling updateAuditContext with
         userId/userEmail/userName (Plan 01 Task 3 pattern);
     (c) captureAuditEvent mockImplementation captures an AuditRowLike
         built from event + ALS;
     (d) expectAuditRowComplete(capturedRow!) asserts the six fields.
7. share/[shareKey]/route.test.ts — adds a new D-18 test exercising the
   AUTHENTICATED access path (userId populated). Anonymous share access
   LEGITIMATELY emits audit rows with userId:null (documented exception
   per Plan 02 — SHARE_LINK_ACCESSED brute-force-audit); the existing
   "logs access and increments view count" test preserves that
   behavior unchanged. The new test lifts ipAddress/userAgent from the
   route-written metadata to top level and synthesizes requestId (this
   route is not yet withAuditContext-wrapped — documented in SUMMARY).
8. users/[userId]/change-password/route.test.ts — adds a new D-18 test.
   Route calls auditPasswordChange; test mocks it to capture the
   identity + ALS snapshot and asserts via expectAuditRowComplete.

Verification:
- All 107 tests across the 8 migrated files pass.
- Project-wide rg expectAuditRowComplete count = 46 (>= 16 gate).
- pnpm type-check exits 0.
- Full test suite delta: 2 new passing tests (+0 new failures). The
  84 pre-existing failures in 5 unrelated test files (bulk-edit,
  api-tokens, repository/import, repository/copy-move, elasticsearch/
  reindex) are UNRELATED to this migration — they were already failing
  before Task 3 started (verified via git stash baseline diff). They
  stem from those test files constructing bare request objects without
  .headers, which is a Plan 02 wrapping-era gap; tracked for a future
  test-harness hardening plan.

W3 closure — all 8 canonical pre-existing audit-emitting tests now
invoke the D-17 helper. With Task 2's share-links.server.test.ts
extension, total D-18 coverage = 9 files.

* fix(audit-64-06): wrap POST /api/issues/[issueId]/sync with withAuditContext (CR-01)

- Closes CR-01 from Phase 64 verification: every user click on the
  refresh-issue button was producing a 500 because the unwrapped
  route left ALS empty and enqueueWithAuditContext (at
  SyncService.ts:207) threw per D-14 loud-failure design.
- Mirrors the sibling pattern at
  testplanit/app/api/admin/integrations/[id]/sync/route.ts:9.
- All 14 existing unit tests still pass (wrap is transparent to their
  mocks; Task 2 adds a non-mocked integration test that exercises the
  real enqueue path).

* test(audit-64-06): add non-mocked integration test for SyncService.queueIssueRefresh (CR-01 regression guard)

- Exercises the REAL SyncService.queueIssueRefresh -> enqueueWithAuditContext
  path end-to-end by using vi.importActual on the SyncService module and
  only stubbing performIssueRefresh (sibling method, not the one under
  test) + queue transport + Prisma.
- Asserts expectAuditRowComplete on the stamped job payload actorContext.
- Would have failed pre-Task-1 (D-14 throw on empty ALS), passes post-wrap.
- Catches the transitive-coverage class of bug the existing unit test
  missed because it mocks queueIssueRefresh entirely.

* feat(audit-64-06): tighten expectAuditRowComplete to reject undefined field values (WR-03)

- D-17 intent is complete actor context; a missing (undefined) field is
  just as incomplete as an explicit null. Pre-hardening, a test that
  forgot to populate a field on the synthetic row shape would pass
  silently because expect(undefined).not.toBeNull() is truthy.
- Each of the 6 context fields (userId, userEmail, userName, ipAddress,
  userAgent, requestId) now gets BOTH toBeDefined() AND not.toBeNull().
- allowSystem:true branch is unchanged (toMatchObject with
  expect.any(String) already rejects undefined).
- Adds 6 new regression tests (one per field); 17 total tests pass.
- D-18 migrated tests (4 files, 56 tests) still pass — no regression.

* feat(audit-64-06): tighten hasAlsIdentity to reject empty-string fields (WR-01)

- Previously hasAlsIdentity used Boolean truthiness on each context
  field, which is safe today (extractIpAddress returns undefined, not
  empty string). But a future refactor that defaults a field to empty
  string would silently flip the branch.
- New local isPresent helper: typeof value === 'string' && value.length > 0.
  Guards the 6 context fields explicitly.
- Adds 2 new regression tests:
  1. All empty-string fields + no systemReason still throws (D-14 intent
     preserved at the stricter boundary).
  2. Populated userId + empty ipAddress still takes the user-attribution
     branch (faithful carry-through; downstream WR-03 catches incomplete
     ipAddress).
- Route integration test (Task 2) still passes; all Phase 64 tests green.

* refactor(audit-64-06): remove redundant getServerSession mock re-issue in share-links CTX-02 test (WR-05)

- The block claimed mockReset above nukes the session mock, but the
  mockReset on L558 was against prisma.auditLog.create, not
  getServerSession. The re-issue was truly dead code.
- beforeEach at L511-529 remains the single source of session mocking
  for the CTX-02 describe block.
- All 28 tests still pass after removal.

* chore(audit-64-06): add TODO(CTX-FOLLOWUP-WR04) breadcrumb for share/[shareKey] wrap deferral

- Sharpens the pre-existing comment at the synthesized requestId
  assertion so a future phase that wraps share/[shareKey]/route.ts can
  grep for CTX-FOLLOWUP-WR04 as starting inventory.
- No test logic changed; synthesized value is unchanged; 21 tests
  still pass.

* fix(audit-64-06): remove unused updateAuditContext import in share-links test (Rule 1 post-WR-05)

Task 5 removed the block that used updateAuditContext, leaving the
import unused and flagged by @typescript-eslint/no-unused-vars (an
error in this repo). Auto-fix per Rule 1.

* fix(64): WR-06 remove double-cast and banned Function type in share-links test

Replaces `(createSpy as unknown as { mockImplementation: Function })`
with a direct call on `createSpy`, which is already typed as a
`MockInstance` via `vi.mocked(prisma.auditLog.create)`. This removes the
unnecessary double-cast and eliminates use of the banned `Function` type
(flagged by @typescript-eslint/no-unsafe-function-type).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(64): repair 84 unit tests broken by Phase 64 route/enqueue wrapping

Four test files (api-tokens, bulk-edit, import, elasticsearch/reindex)
fabricated NextRequest objects without a `headers` property. The new
withAuditContext wrapper calls req.headers.get() at handler entry, which
threw TypeError. Added `headers: new Headers()` to the fake requests.

Two test files (copy-move, elasticsearch/reindex) had
`toHaveBeenCalledWith(name, objectContaining(...))` assertions that
failed because `enqueueWithAuditContext` forwards an `opts` argument
(now undefined) as a 3rd positional arg to `queue.add`. Added
`undefined` as the 3rd expected arg to match the new call shape.

Full suite: 5708/5708 passing.

* chore: stop tracking generated testplanit/dist/ files

main recently added testplanit/dist/ to .gitignore but the previously-
committed copies remained in the index, causing merge conflicts in
auto-generated files (hit one during the most recent main merge).

Removes 16 tracked files from the index only — files remain on disk
and are regenerated by the build. No runtime behavior change.

* refactor(audit-64-07): wrap 4 two-factor auth routes with withAuditContext

- enable/route.ts (POST): wrap user.update for twoFactorEnabled + twoFactorBackupCodes
- disable/route.ts (POST): wrap user.update clearing 2FA fields
- verify/route.ts (POST): wrap user.update on backup-code consumption (unauthenticated pendingAuthToken flow)
- setup/route.ts (GET): wrap user.update for encrypted twoFactorSecret

ALS frame now established before Prisma extension hook fires (lib/prisma.ts:558),
so ip/UA/requestId populate emitted audit rows. Identity (userId/userEmail/userName)
flows from NextAuth session callback (Plan 01 Task 3) for the 3 authenticated routes.
verify/route.ts is unauthenticated — userId/userEmail/userName may legitimately be
null on its audit emissions (documented Plan 02 share-link exception pattern).

* refactor(audit-64-07): wrap 4 integration routes with withAuditContext

- integrations/route.ts (GET + POST): wrap integration.create on POST
- integrations/[id]/route.ts (GET + PUT + DELETE): wrap integration.update (PUT) and integration.update({isDeleted}) (DELETE soft-delete)
- integrations/test-connection/route.ts (POST): wrap integration.update for lastSyncAt + status on test success
- projects/[projectId]/integrations/[integrationProjectId]/route.ts (PATCH + DELETE): wrap projectIntegration.update + projectIntegration.delete

ALS frame established before Prisma extension hooks at lib/prisma.ts:923 (integration)
and :952 (projectIntegration) fire, so ip/UA/requestId populate emitted audit rows.
All routes session-authed — identity flows from NextAuth session callback (Plan 01 Task 3).

* refactor(audit-64-07): wrap 2 admin routes + repair 2 broken tests

Routes wrapped:
- admin/users/verify-all/route.ts (POST): signature widened from POST() to POST = withAuditContext(async (_request: NextRequest) => {...}) to satisfy HOF constraint; wraps user.updateMany → auditBulkUpdate at lib/prisma.ts:599
- admin/prompt-configs/import/route.ts (POST): wraps promptConfig.create at lib/prisma.ts:995

Tests repaired (Plan 06 pattern — fake NextRequest needs .headers for HOF):
- admin/users/verify-all/route.test.ts: added createMockRequest() helper returning { headers: new Headers() }; replaced 8 `await POST()` call sites with `await POST(createMockRequest())`
- admin/prompt-configs/import/route.test.ts: augmented existing createMockRequest(body) helper with `headers: new Headers()` (single-line addition between json and closing brace)

test-connection/route.test.ts unchanged — uses real new NextRequest(url, ...) which provides .headers natively.

42/42 targeted tests pass. Type-check clean.

* chore(audit-64-07): apply prettier formatting to 2 wrapped routes

Prettier reformatted withAuditContext(async (...) => {...}) wrappers in two
multi-param routes to use multi-line argument layout:

  export const VERB = withAuditContext(
    async (
      request: NextRequest,
      { params }: { params: Promise<...> }
    ) => {
      ...
    }
  );

Semantically identical; style-only change. Type-check clean post-format.
Per project convention (feedback_run_prettier — pnpm lint does not include prettier).

* docs(audit-64): document __system__ sentinel and systemReason in user-facing audit log guide

Phase 64 introduced an explicit __system__ userId sentinel for system-initiated
audit events (scheduled jobs, worker-to-worker chained operations) plus a
systemReason metadata field naming the originating job. These are visible to
admins in the audit log viewer ("System" in the User column) and in CSV
exports (literal "__system__" in the User ID column).

Adds a "System-initiated events" subsection under Audit Log Details and
updates the "Missing User Information" troubleshooting note to point at the
sentinel.

* style: run prettier on audit-log-affected files

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant