Skip to content

[comp] Production Deploy#2862

Merged
tofikwest merged 23 commits into
releasefrom
main
May 15, 2026
Merged

[comp] Production Deploy#2862
tofikwest merged 23 commits into
releasefrom
main

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot commented May 15, 2026

This is an automated pull request to release the candidate branch into production, which will trigger a deployment.
It was created by the [Production PR] action.


Summary by cubic

Deploys the cloud-tests auditor visibility improvements: clearer evidence, per-check descriptions, grouping, comments on findings, and a new History tab; also fixes background-check exemptions to persist reason/justification and tightens “Fix All” to failed-only with stricter exception expiry parsing.

  • New Features

    • Evidence viewer on each result with server-side redaction of secrets; copy-to-clipboard.
    • “About this check” panel: AWS checks get a cached Tier 3 description (what/pass/fail/why); GCP/Azure derive descriptions from provider evidence; forbidden control IDs/URLs stripped.
    • Results grouped by check within each service (by checkKey) with “X of Y failing”, display caps, and a renamed “Scan Results” tab; remediation text parsed into steps, reference link, and compliance chips; safer reference URLs.
    • Exceptions and History: mark exceptions with a required reason (20+ chars), optional reviewer, and optional expiry; reconciliation writes FindingResolution/Regression; History tab shows summaries, active exceptions, regressions, and supports revoke.
    • Comments on findings: new finding entity type and an entity-aware permission guard; thread shown on the finding detail sheet.
    • Polish: “Fix All” now targets failed findings only; exception expiry enforces timezone on full ISO timestamps (bare YYYY-MM-DD still expands to end-of-day UTC); stricter, case-insensitive strip of control citations in check descriptions.
  • Migration

    • Adds DB tables and fields for check definitions and history: CheckDefinition, FindingException, FindingResolution, FindingRegression; plus scannedServices/failedServices on IntegrationCheckRun and CommentEntityType.finding.
    • Adds backgroundCheckExemptReason and backgroundCheckExemptJustification on Member; persisted when exempt is true and cleared when set to false.

Written for commit e080cd2. Summary will update on new commits. Review in cubic

tofikwest and others added 18 commits May 13, 2026 18:32
Adds the depth and audit trail Chris asked for so auditors can trust the
automated checks. Five phases shipped together:

Phase 1 — Quick wins
- Evidence JSON viewer on each finding (sensitive keys redacted via
  evidence-sanitizer.ts — suffix-match strategy preserves booleans/numbers)
- Last-scan metadata strip (47 checks, 41 passed, 6 failed, 3.2s)
- Resource labels on every finding row (IAM User: john, S3 Bucket: …)
- Rename "Security Findings" header + tab to "Scan Results"

Phase 2 — AI check descriptions
- Tier 3 panel (what / pass / fail / why) generated lazily on first expand
- Claude Haiku 4.5 via @ai-sdk/anthropic, cached per (orgId, checkId) with
  source-hash invalidation (regenerates when adapter strings change)
- Server-side strip of compliance control numbers + URLs as a backstop
- GCP/Azure: passthrough from provider catalog (no AI call)

Phase 3 — Per-check sub-grouping
- Findings within a service grouped by normalized checkKey
- "X of Y failing" headers; "Show all results" reveals passing rows
- 100-row display cap per check with "Show more" affordance

Phase 4 — Comments on auditor findings
- CommentsPermissionGuard resolves entityType-specific permission so
  auditors (finding:update, no task:update) can comment on findings
- Comments thread on /overview/findings detail sheet

Phase 5 — Resolution + exceptions history
- FindingException / FindingResolution / FindingRegression tables
- Reconciliation classifies each resolution: platform_fix (RemediationAction
  matched), external_fix, resource_deleted, or exception_marked
- scannedServices column on IntegrationCheckRun prevents false "resolved"
  events on partial scans
- "Mark as exception" modal: required reason (20+ chars), optional reviewer,
  optional auto-review date
- New History tab: summary + resolutions + active exceptions + regressions

Includes 96 new tests (59 api + 37 frontend) and 3 Prisma migrations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real bugs flagged by cubic on PR #2838:

- comments-permission.guard.ts: API key / service-token requests were
  granted without any scope check (authorization bypass). Now mirrors the
  standard PermissionGuard's API-key + service-token enforcement, but
  with the dynamic `${entityType}:${action}` scope so finding-permission
  scopes work end-to-end.
- evidence-sanitizer.ts: snake_case credential keys (`access_key_id`,
  `secret_access_key`) and arrays under sensitive keys (e.g. `tokens: [...]`)
  bypassed redaction. Now normalizes case-separators before matching and
  redacts string/object elements of arrays under sensitive keys. Added
  plural variants (tokens / secrets / cookies / accessKeys / signingKeys
  / sessionTokens / publicKeys / privateKeys / apiKeys / passwords).
- exception.service.ts: concurrent markAsException could create duplicate
  active exceptions. New `@@unique([orgId, connectionId, checkId, resourceId])`
  constraint + Prisma upsert guarantees a single row per finding atomically.
  Re-marking after revoke clears `revokedAt` on the same row; full history
  still lives in AuditLog.
- reconciliation.service.ts: idempotency probe only checked FindingResolution
  — a run that produced only regressions could be re-reconciled and write
  duplicate regression rows. Now checks both tables.
- ai-description.prompt.ts: forbidden-content backstop missed bare control
  citations like "CIS 1.8" / "PCI 8.2.3" / "NIST AC-2" / "HIPAA 164.312".
  Added a broader regex catching `(FRAMEWORK) [LETTERS]?[- ]?N(.N)*`.

Smaller cleanups also flagged:

- MarkExceptionModal / cloud-security.controller: date-only exception
  expiry was parsed as UTC midnight, expiring exceptions ~8h before the
  user-selected local date. Frontend now computes `min` from local date,
  backend now expands bare `YYYY-MM-DD` to end-of-day UTC.
- EvidenceJsonViewer copy button: added focus-visible styles so keyboard
  users can see the focused control.
- exception.service.spec.ts: revocation test now asserts revokedAt is a
  Date in the expected window (caught the missing assertion).

Skipped from cubic's list: cloud-security-query.legacy.ts filter — that
code is pre-existing and was not introduced by this PR.

Test counts after fixes: 105 (was 96). 4 Prisma migrations now.

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

Two related follow-ups to the cubic review:

1. Legacy result filter (cubic finding #6 — earlier deferred as pre-existing,
   now fixed since the cwd is right here):

   `getLegacyFindings` used `completedAt <= lastRunAt && completedAt >=
   lastRunAt - 10min` to keep only the most-recent scan's IntegrationResult
   rows. This silently hid the newest legacy failures whenever a scan wrote
   results but did NOT advance `Integration.lastRunAt` (a known consistency
   gap on the legacy path).

   Extracted the filter as a pure `filterToLatestScanResults` helper that
   takes the LATER of `lastRunAt` and the per-integration max result
   `completedAt` as the reference point. Robust against either signal
   being stale. Added 8 unit tests (per-integration scoping, fallback when
   lastRunAt is null, window exclusion, mixed-age data, future-date edge).

2. Prisma migration cleanup:

   The previous `finding_exception_unique` migration was created manually
   (via raw SQL file) because `prisma migrate dev` requires a TTY and
   couldn't run from this environment. `prisma migrate diff` flagged two
   small drift issues afterwards:
     - the manual `CREATE UNIQUE INDEX` left an orphan
       `FindingException_checkId_resourceId_idx` (now subsumed by the
       four-column unique key — should be dropped)
     - the index name didn't match what Prisma's @@unique directive
       expects (truncated differently at the 63-char Postgres limit)

   New migration `finding_exception_index_cleanup`:
     - drops the orphan idx
     - renames the unique index to Prisma's expected name
       (`<Model>_<col1>_<col2>..._key`)

   `prisma migrate status` reports the schema is up to date and
   `prisma migrate diff --from-config-datasource ... --to-schema ...`
   returns an empty migration — drift resolved.

Test counts: 113 (was 105 — added 8 legacy-filter tests).

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

You were right — the two manually-written migrations from prior commits
(`finding_exception_unique` + `finding_exception_index_cleanup`) should
be collapsed into a single migration generated the normal way.

What I did:
- Rolled back the worktree DB to the pre-manual state (dropped the
  manual unique index, restored the original `_checkId_resourceId_idx`,
  removed the two migration rows from `_prisma_migrations`)
- Deleted both manual migration directories
- Ran `prisma migrate dev` properly (used `expect(1)` to handle Prisma's
  interactive `Are you sure?` prompt — the CLI refuses to proceed
  non-interactively when adding a unique constraint that could fail on
  pre-existing duplicates)
- Prisma generated `20260513231103_finding_exception_unique` with the
  exact SQL it would normally produce: DROP the now-redundant idx and
  CREATE UNIQUE INDEX with the conventional truncated name

End state: one clean migration instead of two manuals.
`prisma migrate status` → up to date.
`prisma migrate diff --from-config-datasource ... --to-schema ...`
returns `This is an empty migration.` → zero drift.

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

Two more cubic findings, both real:

1. evidence-sanitizer.ts (P1): `redactArray` handled string and object
   elements but not nested arrays — they fell through `return item`
   unchanged. Payloads like `{ secrets: [["s1","s2"], ["s3"]] }` would
   leak the inner strings. Now recursively redacts.

2. exception-expiry parsing (P2): bare `YYYY-MM-DD` strings like
   "2026-02-30" passed straight into `new Date(Date.UTC(2026, 1, 30))`,
   which silently rolled forward to March 2 — an exception meant to
   expire Feb 30 would actually expire days later than the user expected.
   Extracted to `exception-expiry.utils.ts` with strict component
   validation that throws BadRequestException for non-existent calendar
   dates (Feb 30, month 13, day 0, Feb 29 in non-leap years, etc.).

Test counts: 122 (was 113 — added 1 nested-array sanitizer test + 8
expiry tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The strip showed `Ran Xd ago • N checks evaluated • N passed • N failed`
above the three stat cards. The cards already convey the counts, and
the connection-detail line above the tab already shows `Last scan: …`,
so the strip was double-rendering most of its information.

It could also show numbers that didn't match the cards when an org has
multiple connections of the same provider — the strip's `latestRun`
came from this connection's IntegrationCheckRun, while the cards count
findings from `providerSlug === X OR connectionId === Y` (a pre-existing
OR filter in CloudTestsSection that aggregates across provider
siblings). Confusing for users testing multi-connection orgs.

Removes:
- `<LastScanStrip />` render call
- LastScanStrip component and its formatRelativeTime / formatDuration
  helpers (only used by the strip)
- `latestRun` prop on CloudTestsSection + the `latestRun` pass-through
  in ProviderTabs

Keeps the backend `latestRun` field on the providers API response and
the `ProviderLatestRun` type — both are still emitted (cheap, harmless)
in case a future view wants this data without re-plumbing the API.

The only piece of unique info lost is the scan duration (~3.2s style),
which wasn't surfaced anywhere else. If we want it back, a tighter
"Duration X" pill near the Scan button is a follow-up — separate from
the redundancy fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…compai/comp into tofik/cloud-tests-auditor-visibility
… view

The shape mismatch between the primary Fix pill (accent-filled, with
icon) and the Mark-as-exception button (rectangular, outlined) made
every failing row look visually noisy. Worse, both competing for the
same right-side action area pushed the severity badge around.

Marking a finding as an exception is meant to be a DELIBERATE action —
the user must provide a written reason that an auditor will read. So
forcing them to expand the row first (read the title, the description,
the evidence, the remediation) before they can mark it accepted is
actually better auditor-experience UX, not worse.

- Removes the row-level "Mark as exception" button
- Adds it to the bottom of the expanded panel, right-aligned, separated
  by a divider so it reads as a discrete final action
- All the modal wiring stays exactly the same — same canMarkException
  gate (`integration:update` permission), same onMarkException
  callback, same MarkExceptionModal opens

Row right side is now just [Fix] [severity-badge] — clean and
consistent.

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

For findings like "No CloudTrail trails configured" or "No GuardDuty
detector", the AI fix-plan dialog rendered CURRENT and PROPOSED as
empty {} boxes — the user couldn't see what the fix was about to create.

Root cause: the SYSTEM_PROMPT for fix plans tells the model to populate
currentState/proposedState ONLY with fields from the scan evidence, and
to "show absence as false or null". For mutate-existing-resource
remediations that worked fine. But for create-from-scratch remediations
the evidence has nothing to describe (the resource doesn't exist), so
the model returned `{}` for both — collapsing to an empty diff.

Adds a new "CREATE-FROM-SCRATCH REMEDIATIONS" section to the prompt
that:

- Requires currentState to include `"exists": false` (plus other known
  absence facts) so the user sees that nothing is there today.
- Requires proposedState to describe the resource that WILL be created
  using the same values the model is putting into its fixSteps
  (CreateTrail name, multi-region flag, log validation, bucket name,
  etc.) — concrete, not abstract.
- Explicitly forbids returning empty objects for both. At minimum the
  model must emit `{ exists: false }` / `{ exists: true }` so the
  user sees a readable false→true diff.

No schema change — `currentState`/`proposedState` are already
`z.record(z.string(), z.unknown())`. No UI change either — the existing
StateBlock renders whatever JSON the AI returns. The fix is purely on
the prompt side.

Other findings likely affected by the same pattern (now improved):
"No multi-region CloudTrail", "No AWS Config recorder", "No GuardDuty
detector", "No CloudWatch log group for X", "No S3 bucket configured
as the log target", etc.

Reported by sales via Tofik — empty CURRENT/PROPOSED boxes on the
"No CloudTrail trails configured" Auto-Remediate dialog.

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

The previous prompt change asked the AI to emit `{ exists: false }` /
`{ exists: true, ... }` for create-from-scratch findings, but the model
doesn't reliably follow it — Tofik still sees `{} → {}` in the
Auto-Remediate Finding dialog for "No CloudTrail trails configured"
even with the new prompt in place.

Server-side backstop: after `generateFixPlan` and `refineFixPlan`
return, if BOTH currentState and proposedState are empty, fill them in
deterministically. `currentState` becomes `{ exists: false }`.
`proposedState` becomes `{ exists: true }` or, when the plan's fixSteps
include any `Create*Command` ops, `{ exists: true, willCreate:
["service:Resource", ...] }` derived from those steps. Never depends on
the model.

Only kicks in when BOTH sides are empty — verify-only plans that
legitimately have one side blank are untouched. Mutate-existing plans
(IAM, S3 versioning, etc.) already have rich state, so they're
untouched too. Risk of regression on the working cases: zero.

Includes ai-remediation.service.spec.ts (4 tests):
- CloudTrail-style with Create* steps → currentState { exists: false },
  proposedState { exists: true, willCreate: ["cloudtrail:Trail",
  "s3:Bucket"] }
- Plan with no Create* steps → proposedState falls back to `{ exists:
  true }` without a willCreate list
- Plan with non-empty currentState → untouched
- Plan with one side empty → untouched (legitimate verify-only)

The prompt change from 11d717f stays in — it's still useful when the
model does follow it (richer output than the backstop). This fix
guarantees a readable diff regardless of model behavior.

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

GCP/Azure passthrough was using the SCC `description` for both
"description" and "fail criteria", causing the same paragraph to
appear under multiple labels in the expanded finding view. Each of
the four fields (description, passCriteria, failCriteria,
whyItMatters) now derives a distinct sentence from the machine-
readable category code.

Remediation was rendered as a single paragraph that included the
reference URL and compliance metadata inline. A new
`parseRemediation` helper splits the API-concatenated string back
into `{ steps, referenceUrl, compliance[] }`, and a new
`RemediationSection` component renders steps as readable text, the
reference as a link, and frameworks as chips.

Also tightened the visual hierarchy of the expanded view so the
"about this check" card, the per-account failure, the evidence
viewer, and the remediation card each have a distinct treatment
instead of looking like five identical white boxes.

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

P1 — RemediationSection now validates the reference URL scheme before
assigning it to an href. `parseRemediation` extracts the URL from
provider-supplied text (today: SCC's externalUri); a new `safeHttpUrl`
helper only honors http(s) URLs and rejects javascript:, data:, vbscript:,
file:, and protocol-relative URLs. The link is hidden when validation
fails so we never render an unsafe href.

P2 — enrichEmptyState was firing for every {}/{} plan, including
update-style remediations (e.g. UpdateAccountPasswordPolicyCommand),
fabricating `currentState: { exists: false } → proposedState: { exists: true }`
which told the UI "we'll create this resource" when the truth was
"we'll update the existing one". The backstop now only enriches when
at least one `Create*` command is present in fixSteps. Update-style
plans are left untouched.

Tests:
- 10 new safeHttpUrl tests (parser test file)
- Updated the existing backstop test that asserted the wrong behavior
- Added `purpose` to fix-step test data to satisfy the strict schema

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eight real issues from cubic's review on 79bfb0f. Two findings about
Jest hoisting (exception/reconciliation specs) skipped — those are
babel-jest's rule but the project uses ts-jest, where the pattern works
and all 145 tests currently pass.

P3 — "All passed" badge unreachable
  CloudTestsSection.tsx used `group.findings.length > 0` for hasFailures,
  but `findings` is the merged failed+passed set, so groups with only
  passing checks were never recognized. Use `group.failed > 0` instead.

P2 — Dangling FK on FindingRegression.previousResolutionId
  Schema documented it as an FK but didn't declare a relation. Added
  `@relation(..., onDelete: SetNull)` plus a back-relation on
  FindingResolution.regressions, with a Prisma-generated migration that
  adds the constraint.

P2 — DTO accepted whitespace-only exception reasons
  @minlength(20) counts spaces. Added @matches(/(?:\\S.*?){20}/s) so
  twenty literal spaces no longer satisfy the contract.

P2 — Placeholder leak in fix-plan prompt
  CREATE-FROM-SCRATCH example used `<accountId>` while the prompt
  forbids placeholders. Replaced with a concrete account ID + region
  pair so the model has a clean reference.

P2 — exception-expiry fallback was too permissive
  `new Date(input)` accepted locale-specific strings like
  "January 1, 2026" and "2026/08/13", bypassing the documented
  ISO 8601 contract. Added a strict ISO 8601 regex check ahead of the
  Date constructor; spec updated to assert the new strict behavior.

P2 — History tab silently truncated rows
  Service capped resolutions/exceptions/regressions at 200/100 with no
  totals. Now returns true counts from separate `count()` queries plus
  a `truncated` flag per category; HistoryTab renders "shown of total"
  when truncation is active so auditors don't see misleading numbers.

P2 — CommentsPermissionGuard missing try/catch around checkPermission
  Standard PermissionGuard wraps the call so network/auth-service
  failures convert to ForbiddenException('Unable to verify permissions')
  rather than leaking 500s. Mirrored that here.

P2 — platform_fix attribution scope too broad
  Reconciliation looked up RemediationActions by connection + resource
  alone, so a fix for check A on a resource was mis-attributed to
  check B on the same resource. Plumbed `priorCheckResultId` through
  indexResults → determineResolutionMethod and now filter by the
  specific `checkResultId` instead. Spec updated to populate `id` on
  test results.

Test counts: 145 API tests + 20 parser tests pass; the single failing
suite (remediation.controller.spec.ts) is the pre-existing Postgres
TLS environmental issue, unchanged.

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

feat(cloud-tests): auditor visibility improvements (phases 1-5)
@vercel
Copy link
Copy Markdown

vercel Bot commented May 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app (staging) Ready Ready Preview, Comment May 15, 2026 10:58pm
comp-framework-editor (staging) Ready Ready Preview, Comment May 15, 2026 10:58pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
portal (staging) Skipped Skipped May 15, 2026 10:58pm

Request Review

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 64 files

Confidence score: 3/5

  • There is concrete regression risk: in apps/app/src/app/(app)/[orgId]/cloud-tests/components/CloudTestsSection.tsx, the Fix All targeting can now include passed findings, which is user-facing and could trigger unintended batch actions.
  • The highest-severity issue is in apps/api/src/cloud-security/reconciliation.service.spec.ts: the hoisted jest.mock() referencing dbMock may break test execution unless the mock variable naming/placement is adjusted.
  • apps/api/src/cloud-security/exception-expiry.utils.ts has environment-dependent datetime parsing without explicit timezone offsets, which can produce inconsistent expiry behavior across servers.
  • Pay close attention to apps/app/src/app/(app)/[orgId]/cloud-tests/components/CloudTestsSection.tsx, apps/api/src/cloud-security/reconciliation.service.spec.ts, and apps/api/src/cloud-security/exception-expiry.utils.ts - unintended fix targeting, mock hoisting safety, and timezone-sensitive expiry logic are the key risks.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/api/src/cloud-security/exception-expiry.utils.ts">

<violation number="1" location="apps/api/src/cloud-security/exception-expiry.utils.ts:50">
P2: Require an explicit timezone offset for timestamp inputs. Allowing timezone-less datetimes causes environment-dependent expiry values when `new Date(input)` parses in server local time.</violation>
</file>

<file name="apps/api/src/cloud-security/reconciliation.service.ts">

<violation number="1" location="apps/api/src/cloud-security/reconciliation.service.ts:127">
P2: Redundant condition: `!prior.passed === false` is equivalent to `prior.passed` (due to `!` binding tighter than `===`), making it a duplicate of the next line `if (prior.passed) continue`. Remove this line — it adds confusion and likely masks a logic error the author originally intended (e.g., filtering on a different field).</violation>
</file>

<file name="apps/app/src/app/(app)/[orgId]/cloud-tests/components/CloudTestsSection.tsx">

<violation number="1" location="apps/app/src/app/(app)/[orgId]/cloud-tests/components/CloudTestsSection.tsx:359">
P1: `Fix All` now includes passed findings because service groups store passed+failed results, but batch target selection still filters only by remediation key.</violation>
</file>

<file name="apps/api/src/cloud-security/ai-description.prompt.ts">

<violation number="1" location="apps/api/src/cloud-security/ai-description.prompt.ts:109">
P2: Make the ISO control-number regex case-insensitive so lowercase variants are also blocked.</violation>
</file>

<file name="apps/api/src/cloud-security/reconciliation.service.spec.ts">

<violation number="1" location="apps/api/src/cloud-security/reconciliation.service.spec.ts:24">
P1: Rename `dbMock` to a `mock*` variable (or inline the factory) so the hoisted `jest.mock()` can access it safely.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic

Comment thread apps/api/src/cloud-security/reconciliation.service.spec.ts
Comment thread apps/api/src/cloud-security/exception-expiry.utils.ts Outdated
Comment thread apps/api/src/cloud-security/reconciliation.service.ts Outdated
Comment thread apps/api/src/cloud-security/ai-description.prompt.ts Outdated
tofikwest and others added 3 commits May 15, 2026 18:15
…mber

"Confirm exemption" was failing for every customer with a "Failed to
confirm exemption" toast. Root cause:

  apps/api/src/main.ts sets `forbidNonWhitelisted: true` on the global
  ValidationPipe. The V1 frontend sends `backgroundCheckExempt` together
  with `backgroundCheckExemptReason` + `backgroundCheckExemptJustification`,
  but the latter two were never declared on UpdatePeopleDto — so the
  PATCH /v1/people/:id request was rejected with 400 before the service
  ever ran.

This fix:

1. Adds nullable columns `backgroundCheckExemptReason` (varchar) and
   `backgroundCheckExemptJustification` (text) to the Member model
   (named prisma migrate dev migration — not hand-written SQL).
2. Whitelists both fields on UpdatePeopleDto as optional strings with
   sensible length caps (100 / 2000 chars).
3. Persists both fields on member.update; clears them to null when
   backgroundCheckExempt is set to false so a future re-exemption starts
   from a clean state. Audit log retains the prior values from the
   original exempt-true request via AuditLogInterceptor.
4. Adds 4 unit tests in member-queries.spec.ts covering: persist on
   exempt=true, clear on exempt=false, clear overrides incoming stale
   reason on exempt=false, untouched when patch omits exempt.

Pre-existing typecheck failures in unrelated specs and a pre-existing
people.service.spec runtime error were verified to also occur on the
baseline (origin/main) before this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Prisma schema change in this branch added two non-optional (nullable)
columns to Member — backgroundCheckExemptReason + backgroundCheckExemptJustification.
The strict-typed test mock createMockMember returns Member, so the mock
had to include both keys or the spread of overrides would surface them
as undefined and fail the Vercel build:

  Type 'undefined' is not assignable to type 'string | null'.

Initializing both to null in the default mock matches the DB default.

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

fix(background-check): persist exemption reason + justification on member
Four real issues caught by cubic on the main→release deploy review.
The two Jest-hoisting findings (exception/reconciliation specs) were
skipped — that rule is babel-jest only, project uses ts-jest, all
tests pass in CI.

P1 — "Fix All" could target passing findings
  Service groups store the merged failed+passed set, and
  `canFixFinding` returns a key for any finding with a `findingKey`
  regardless of status. The batch dialog was happily including
  already-passing checks in the remediation target list. Now filter
  by `status === 'failed'` before consulting canFixFinding.

P2 — exception-expiry accepted timezone-less timestamps
  ISO 8601 regex made the timezone offset optional, so
  `2026-08-13T23:59:59` passed validation but `new Date()` parsed
  it in server-local time — same input, different expiry on
  UTC vs Pacific hosts. Made the offset required; updated the
  spec to assert both acceptance (with offset) and rejection
  (without offset).

P2 — dead `!prior.passed === false` in reconciliation
  The line evaluated identically to the very next `if (prior.passed)
  continue`. Removed; behavior unchanged, clarity restored.

P2 — ISO control-number regex was case-sensitive
  `/\bA\.\d+\.\d+(\.\d+)?\b/` had no /i flag, so lowercase
  variants like "a.5.1.2" would slip past the forbidden-content
  guard. Added /i and a regression test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(cloud-tests): cubic follow-ups from production deploy review
@vercel vercel Bot temporarily deployed to staging – portal May 15, 2026 22:55 Inactive
@tofikwest tofikwest merged commit ca88b16 into release May 15, 2026
13 checks passed
@claudfuen
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.56.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants