feat(support): ship support MVP and Coolify-ready deployment path - #47
Andreas-Froyland wants to merge 391 commits into
Conversation
Landed in 504ee57.
…al contract
Written before SUP-04-7 landed, against a guessed shape. The real composer
POST body (components/support/SupportComposer.vue) and presign response use
{ storageKey, fileName, contentType, sizeBytes } - no cid, since this
composer has no inline-image insertion. Fixed the field name mismatch
(filename -> fileName) that would have made every attachment silently drop
on send (zod strips unknown keys rather than rejecting them).
Also added the per-message attachment total-size enforcement that
parallel-agents.md flags as explicitly this endpoint's job:
MAX_MESSAGE_ATTACHMENT_BYTES (25 MB) is checked here because SUP-04-7's
presign step validates one file at a time and cannot see the others in the
same reply. Extracted as totalAttachmentBytes() in outbound-reply.ts so it
has a real test rather than living untested inside the zod schema.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…4-11 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fires from the inbound endpoint when an inbound message opens a NEW conversation on an inbox with autoReplyEnabled. All four required guards: 1. Never on a detected auto-response - enforced by control flow, the isAutoResponse check already returns before this code is reached. 2/3. Never on an existing conversation, never more than once per conversation - both collapse into isNewConversation (server/utils/ auto-reply.ts's shouldSendAutoReply): the only way this branch runs is "new conversation", which happens at most once by definition. 4. Auto-Submitted: auto-replied set on the outgoing message, so the other end's loop detection works. Plus the fifth requirement from the stage doc, per-contact rate limiting (1/hour via the existing rate-limit store, keyed by contactId rather than IP - this isn't a per-request limit). Reuses buildOutgoingReply (SUP-04-4) for composition rather than a separate send path, then layers Auto-Submitted on top (server/utils/auto-reply.ts). Enqueues to the same durable outbox inside the same transaction as the incoming message and conversation insert. Two deliberate judgment calls, flagged in parallel-agents.md for Stage 06: - Does not touch firstResponseAt/lastAgentReplyAt - a system acknowledgment is not a substantive agent response, but design.md never addresses this interaction directly. - No sending address or no inbound Message-ID to thread onto: logged and skipped rather than failing the webhook, since there is no agent to show an error to and the customer's message must still be ticketed. 8 new unit tests (auto-reply.ts's pure composition and guard logic). The endpoint wiring itself has no dedicated test - same gap as SUP-04-4, no precedent in this codebase for unit-testing endpoint handlers directly. harness:verify green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o support-platform Agent 1's outgoing-reply wiring, note-never-dispatches enforcement, and auto-reply (all four guards) merged clean; harness:verify green (E2E/Redis/Postgres skip, no Docker).
…4-10) Fixes a real gap found while building this: completeOutboundDelivery and failOutboundDelivery only ever updated supportOutboundDelivery.status, never conversationMessage.deliveryStatus -- a message would show 'pending' in the UI forever regardless of whether the send actually succeeded or exhausted its retries. Both functions (plus resetOutboundDeliveryForRetry) now take (id, messageId) and update both rows in one transaction, and publish a realtime event on the conversation/inbox channels so the existing subscribeConversation reload picks up the change without any client-side event-type handling. Adds POST /api/support/conversations/[id]/messages/[messageId]/retry (409s unless kind:'outgoing' and deliveryStatus:'failed'), and renders all five deliveryStatus values plus a retry button in SupportMessageItem.vue. This crosses into server/utils/outbound-delivery.ts (Agent 1's file this stage) -- flagged and documented in parallel-agents.md, same as Agent 1's own fix of my SUP-04-7 contract guess in SUP-04-4.
Landed in d57b595.
…ply -> customer reply threads back (SUP-04-11) tests/e2e/support-outbound-reply.spec.ts, guarded like support-inbound-email.spec.ts (skips without Postmark webhook creds): reply-before-sending-address is 409, a note with attachments is 400, an outgoing reply's channelMessageId is set synchronously, deliveryStatus leaves 'pending' within 5s (regression coverage for the SUP-04-10 bug this stage found), and a simulated customer reply referencing that channelMessageId lands on the same conversation rather than opening a new one -- the stage's headline threading risk, exercised end to end for the first time rather than only unit-tested on the composition side. Written and typechecked/linted clean; not executed this session -- no Docker, no reachable Postgres, no Postmark credentials on this box, same as every other guarded suite this stage (and the same state SUP-03-14 was explicitly flagged in). Descopes acceptance criterion 1 (Gmail/Outlook threading) and "contact has no email" (unreachable via the email channel) deliberately, per parallel-agents.md.
Landed in 68d104b. Written, not executed this session -- no Docker.
… (SUP-04-9) POST /api/support/delivery/[provider] maps Postmark/Mailgun delivery, bounce, and engagement events onto conversationMessage.deliveryStatus. A hard bounce marks the message bounced and writes a visible activity line (both in one transaction); a soft bounce is recorded but leaves deliveryStatus untouched, since the provider's own SMTP retry is still in flight and the message already left our outbox successfully. ChannelDriver grows parseDeliveryEvent/extractDeliveryEventId on both drivers. verifySignature/isConfigured are reused unchanged: both providers protect every webhook URL on an account identically (Postmark: Basic Auth in the URL; Mailgun: the HMAC envelope), so there is nothing delivery-specific to verify differently. Parsing here is pure and in-memory (no raw-body archival, unlike inbound), so it happens before the claim rather than after - recordType/recipient are real NOT NULL columns and are already known by claim time, which avoids a second migration. THE BIGGEST UNCONFIRMED ASSUMPTION THIS STAGE: DeliveryEvent.messageId is read from whatever field each provider's webhook uses to identify the original message, on the assumption it equals the RFC Message-ID this app set when sending. That is documented as true for a provider's HTTP send API. It has NOT been verified for SMTP relay, which is what this app actually uses (lib/email.ts -> nodemailer -> SMTP_HOST) - a provider receiving mail over SMTP may track by an internal id that never appears in the RFC header at all. If so, delivery-status tracking silently correlates nothing, regardless of how correct the rest of this code is. Handled as data, not an error, matching D-35's precedent (messageId: null, recorded and returned 200) - but this needs an empirical check against a real send before anyone trusts it. Full reasoning in parallel-agents.md. Two more judgment calls, both flagged in code and doc: - Bounce types this driver does not specifically recognize default to hard, not soft - design.md's stated priority is that silent delivery failure is worse than a visible error. - Engagement events (opened/clicked/spam_complaint) are recorded for audit and future stages but do not change deliveryStatus - out of this stage's acceptance criteria. Found and fixed one real bug in review before it shipped: a test fixture used a Postmark bounce ID (4323372036854775807) large enough to lose precision as a float64, silently producing the wrong idempotency key. Not a real Postmark ID - my own fixture mistake. Replaced with a realistic magnitude; flagged the theoretical risk in a comment since JSON.parse has already parsed the number by the time this code sees it. 14 new unit tests, fixture-based against documented Postmark/Mailgun payload shapes (support-channels-delivery.test.ts). 5 new guarded integration tests for the claim/lease atomicity (delivery-events.test.ts), NOT executed against real Postgres this session - no Docker on this box, same gap as every other guarded suite this stage. Verified structural correctness via typecheck and a real connection attempt (ECONNREFUSED, not an import error). harness:verify green (402 total unit tests via the default runner). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every checkbox in parallel-agents.md's work split is now checked. The SMTP-relay message-id correlation assumption is flagged as the highest-priority thing to verify against a real send before trusting delivery-status tracking in production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found running the suite against real Postgres (Docker up for the first time this session): "enqueues a pending row..." and "rejects a second enqueue..." each left their supportOutboundDelivery row 'pending' with no cleanup. claimNextOutboundDelivery takes the oldest pending row across the whole table, unscoped to a test, so those leftovers got claimed ahead of the next test's own row -- "claims the oldest pending row" and "does not reclaim a row whose lease is still live" failed with a messageId mismatch that had nothing to do with the code under test. Both tests now complete their row, matching the cleanup pattern already used elsewhere in this file. All 8 tests pass against real Postgres after the fix. README.md's status table is auto-updated by the harness docs-map check.
Agent 1's delivery/bounce webhook (2f0503c, fd01de5) merged clean; verified against real Postgres/Redis this session (Docker up) -- 34/34 Postgres integration tests pass including delivery-events.test.ts's real-concurrency race test, 6/6 Redis integration tests pass, harness:verify green end-to-end.
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
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="tests/e2e/support-outbound-reply.spec.ts">
<violation number="1" location="tests/e2e/support-outbound-reply.spec.ts:668">
P3: `await firstProxyUploadStarted` has no timeout. If the first proxy upload never reaches the route handler (e.g., a client regression flips the attachment to `failed`, or the real presign starts returning a direct URL in the test environment), this promise never resolves and the test — and everything after it in the `test.describe.serial` block — hangs until the test timeout (60s CI / 90s local). The assertion it replaced (`toBeVisible()` with the default 10-15s expect timeout) would have failed fast with a diagnostic. Race the promise against a timeout so the failure is reported as a clear assertion error instead of a hang.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| await expect(composer).toBeVisible({ timeout: 30_000 }) | ||
| const input = page.locator('[data-testid="support-composer-file-input"]') | ||
| await input.setInputFiles({ name: 'contract.txt', mimeType: 'text/plain', buffer: Buffer.from('proxy bytes') }) | ||
| await firstProxyUploadStarted |
There was a problem hiding this comment.
P3: await firstProxyUploadStarted has no timeout. If the first proxy upload never reaches the route handler (e.g., a client regression flips the attachment to failed, or the real presign starts returning a direct URL in the test environment), this promise never resolves and the test — and everything after it in the test.describe.serial block — hangs until the test timeout (60s CI / 90s local). The assertion it replaced (toBeVisible() with the default 10-15s expect timeout) would have failed fast with a diagnostic. Race the promise against a timeout so the failure is reported as a clear assertion error instead of a hang.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/e2e/support-outbound-reply.spec.ts, line 668:
<comment>`await firstProxyUploadStarted` has no timeout. If the first proxy upload never reaches the route handler (e.g., a client regression flips the attachment to `failed`, or the real presign starts returning a direct URL in the test environment), this promise never resolves and the test — and everything after it in the `test.describe.serial` block — hangs until the test timeout (60s CI / 90s local). The assertion it replaced (`toBeVisible()` with the default 10-15s expect timeout) would have failed fast with a diagnostic. Race the promise against a timeout so the failure is reported as a clear assertion error instead of a hang.</comment>
<file context>
@@ -648,9 +665,14 @@ test.describe.serial('outbound attachment contract', () => {
- await expect(
- page.locator('[data-testid^="support-composer-attachment-"][data-phase="uploading"]').first()
- ).toBeVisible()
+ await firstProxyUploadStarted
+ try {
+ await expect(
</file context>
| await firstProxyUploadStarted | |
| await Promise.race([ | |
| firstProxyUploadStarted, | |
| new Promise((_, reject) => setTimeout(() => reject(new Error('proxy upload never started')), 10_000)), | |
| ]) |
There was a problem hiding this comment.
Fixed in 7e7959e. The upload route counter is now awaited with Playwright expect.poll({ timeout: 10_000 }), so missing proxy interception fails with a bounded diagnostic instead of hanging the serial suite. The targeted attachment workflow and full harness pass.
There was a problem hiding this comment.
6 issues found across 23 files (changes from recent commits).
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="docs/plans/2026-09-21-coolify-deployment.md">
<violation number="1" location="docs/plans/2026-09-21-coolify-deployment.md:86">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
The `NUXT_NODEMAILER` JSON override cannot add missing SMTP keys or `auth` at runtime. This app defines Nodemailer from `MAIL_FROM`/`SMTP_*`, and the module supports individual `NUXT_NODEMAILER_*` overrides only for options already present; document the supported variables or change the runtime configuration to define those options unconditionally.</violation>
</file>
<file name="server/database/connection-config.ts">
<violation number="1" location="server/database/connection-config.ts:85">
P2: Malformed query ports can silently fall back to a valid prefix, potentially connecting migrations and the app to the wrong PostgreSQL endpoint. Parse the entire value and require an integer in the valid TCP port range before constructing the config.</violation>
<violation number="2" location="server/database/connection-config.ts:90">
P2: Database names containing URL-encoded reserved characters are passed to PostgreSQL with the percent escapes still present. Decode this already-isolated path component with `decodeURIComponent` so valid `DATABASE_URL` database names resolve correctly.</violation>
</file>
<file name="docs/plans/2026-08-11-support-platform/stage-09-wave-1-implementation.md">
<violation number="1" location="docs/plans/2026-08-11-support-platform/stage-09-wave-1-implementation.md:62">
P2: This plan is committed with every task still unchecked and instructions to "Create ..." and "confirm the missing behavior", yet the deliverables it describes already exist and pass at HEAD (support-reporting-range.ts, support-reporting-volume.ts, and their tests). A future dispatcher following the README dispatch protocol would treat Wave 1 as pending and re-dispatch or re-verify it. Mark the plan as complete/superseded — mirror the status line used in stage-09-semantics.md — or note that Wave 1 shipped and only Wave 2 remains open.</violation>
</file>
<file name="tests/integration/support-reporting-volume.test.ts">
<violation number="1" location="tests/integration/support-reporting-volume.test.ts:389">
P3: The sticky `mockReturnValue` is only undone on the success path. If the injected recompute ever stops throwing (any behavior change to `randomUUID` usage or validation order), the `rejects.toThrow()` assertion fails, the restore line is skipped, and every later `recomputeSupportVolumeDay` call in this file generates the duplicate id `unrelated_${suffix}`, failing with an unrelated duplicate-key error that obscures the real failure. Restore in `try/finally`, or use `mockImplementationOnce` for a single poisoned UUID.</violation>
</file>
<file name="tests/database-connection-config.test.ts">
<violation number="1" location="tests/database-connection-config.test.ts:29">
P3: This guard isolates the `playwright_e2e` job with whitespace-exact string splits (`' playwright_e2e:'` / `'\n delete_neon_branch:'`) of a YAML file. Renaming or reordering `delete_neon_branch`, or reformatting job indentation, silently degrades the split (the assertion then scans the rest of the file, or the test fails spuriously), so the TLS regression guard becomes brittle. Since the goal is only to prevent relaxing `DATABASE_SSL_MODE` in this workflow, assert on the whole file instead of parsing job boundaries by exact indentation.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| Use `.env.example` for local/build variables and direct process-environment consumers; this plan | ||
| documents the production Nuxt runtime mappings. `NUXT_STORAGE_DIRECT_UPLOAD_CONSTRAINTS` defaults to | ||
| `proxy-required`; retain that unless the chosen S3 provider's constraint enforcement is verified. | ||
| The parent `NUXT_NODEMAILER` JSON override supplies keys even when a secret-free build omitted |
There was a problem hiding this comment.
P2: Custom agent: Flag AI Slop and Fabricated Changes
The NUXT_NODEMAILER JSON override cannot add missing SMTP keys or auth at runtime. This app defines Nodemailer from MAIL_FROM/SMTP_*, and the module supports individual NUXT_NODEMAILER_* overrides only for options already present; document the supported variables or change the runtime configuration to define those options unconditionally.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/plans/2026-09-21-coolify-deployment.md, line 86:
<comment>The `NUXT_NODEMAILER` JSON override cannot add missing SMTP keys or `auth` at runtime. This app defines Nodemailer from `MAIL_FROM`/`SMTP_*`, and the module supports individual `NUXT_NODEMAILER_*` overrides only for options already present; document the supported variables or change the runtime configuration to define those options unconditionally.</comment>
<file context>
@@ -0,0 +1,154 @@
+Use `.env.example` for local/build variables and direct process-environment consumers; this plan
+documents the production Nuxt runtime mappings. `NUXT_STORAGE_DIRECT_UPLOAD_CONSTRAINTS` defaults to
+`proxy-required`; retain that unless the chosen S3 provider's constraint enforcement is verified.
+The parent `NUXT_NODEMAILER` JSON override supplies keys even when a secret-free build omitted
+`auth` or undefined SMTP defaults. Nested overrides alone cannot introduce absent keys. Use a full
+mailbox string such as `Veerify <noreply@example.com>` for `from`; `MAIL_FROM_NAME` is not mapped by
</file context>
| const password = queryPassword || decodeUrlPart(url.password) | ||
| const host = (queryHost || url.hostname).replace(/^\[|\]$/g, '') | ||
| const portValue = queryPort || url.port || env.PGPORT || '5432' | ||
| const port = Number.parseInt(portValue, 10) |
There was a problem hiding this comment.
P2: Malformed query ports can silently fall back to a valid prefix, potentially connecting migrations and the app to the wrong PostgreSQL endpoint. Parse the entire value and require an integer in the valid TCP port range before constructing the config.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/database/connection-config.ts, line 85:
<comment>Malformed query ports can silently fall back to a valid prefix, potentially connecting migrations and the app to the wrong PostgreSQL endpoint. Parse the entire value and require an integer in the valid TCP port range before constructing the config.</comment>
<file context>
@@ -0,0 +1,143 @@
+ const password = queryPassword || decodeUrlPart(url.password)
+ const host = (queryHost || url.hostname).replace(/^\[|\]$/g, '')
+ const portValue = queryPort || url.port || env.PGPORT || '5432'
+ const port = Number.parseInt(portValue, 10)
+ if (!Number.isFinite(port)) throw new Error('DATABASE_URL must be a valid PostgreSQL URL or Unix socket path')
+
</file context>
|
|
||
| const databasePath = url.pathname.startsWith('/') ? url.pathname.slice(1) : url.pathname | ||
| const database = | ||
| decodeUrlPart(databasePath, decodeURI) || env.PGDATABASE || user || env.PGUSER || env.USER || 'veerifydb' |
There was a problem hiding this comment.
P2: Database names containing URL-encoded reserved characters are passed to PostgreSQL with the percent escapes still present. Decode this already-isolated path component with decodeURIComponent so valid DATABASE_URL database names resolve correctly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/database/connection-config.ts, line 90:
<comment>Database names containing URL-encoded reserved characters are passed to PostgreSQL with the percent escapes still present. Decode this already-isolated path component with `decodeURIComponent` so valid `DATABASE_URL` database names resolve correctly.</comment>
<file context>
@@ -0,0 +1,143 @@
+
+ const databasePath = url.pathname.startsWith('/') ? url.pathname.slice(1) : url.pathname
+ const database =
+ decodeUrlPart(databasePath, decodeURI) || env.PGDATABASE || user || env.PGUSER || env.USER || 'veerifydb'
+
+ return {
</file context>
| expect(range.days.map((day) => day.date)).toEqual(['2026-03-08']) | ||
| ``` | ||
|
|
||
| - [ ] Run `yarn vitest run tests/support-reporting-range.test.ts` and confirm the missing behavior. |
There was a problem hiding this comment.
P2: This plan is committed with every task still unchecked and instructions to "Create ..." and "confirm the missing behavior", yet the deliverables it describes already exist and pass at HEAD (support-reporting-range.ts, support-reporting-volume.ts, and their tests). A future dispatcher following the README dispatch protocol would treat Wave 1 as pending and re-dispatch or re-verify it. Mark the plan as complete/superseded — mirror the status line used in stage-09-semantics.md — or note that Wave 1 shipped and only Wave 2 remains open.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/plans/2026-08-11-support-platform/stage-09-wave-1-implementation.md, line 62:
<comment>This plan is committed with every task still unchecked and instructions to "Create ..." and "confirm the missing behavior", yet the deliverables it describes already exist and pass at HEAD (support-reporting-range.ts, support-reporting-volume.ts, and their tests). A future dispatcher following the README dispatch protocol would treat Wave 1 as pending and re-dispatch or re-verify it. Mark the plan as complete/superseded — mirror the status line used in stage-09-semantics.md — or note that Wave 1 shipped and only Wave 2 remains open.</comment>
<file context>
@@ -0,0 +1,124 @@
+expect(range.days.map((day) => day.date)).toEqual(['2026-03-08'])
+```
+
+- [ ] Run `yarn vitest run tests/support-reporting-range.test.ts` and confirm the missing behavior.
+- [ ] Implement using `reportingDateAt`/`reportingDayBounds`. Validate date strings before using
+ UTC calendar arithmetic to enumerate labels (not instants). Count the 366 limit in civil date
</file context>
| .from(supportMetricDaily) | ||
| .where(eq(supportMetricDaily.teamId, ids.team)) | ||
| .orderBy(asc(supportMetricDaily.id)) | ||
| randomUuidMock.fn.mockReturnValue(`unrelated_${suffix}`) |
There was a problem hiding this comment.
P3: The sticky mockReturnValue is only undone on the success path. If the injected recompute ever stops throwing (any behavior change to randomUUID usage or validation order), the rejects.toThrow() assertion fails, the restore line is skipped, and every later recomputeSupportVolumeDay call in this file generates the duplicate id unrelated_${suffix}, failing with an unrelated duplicate-key error that obscures the real failure. Restore in try/finally, or use mockImplementationOnce for a single poisoned UUID.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/integration/support-reporting-volume.test.ts, line 389:
<comment>The sticky `mockReturnValue` is only undone on the success path. If the injected recompute ever stops throwing (any behavior change to `randomUUID` usage or validation order), the `rejects.toThrow()` assertion fails, the restore line is skipped, and every later `recomputeSupportVolumeDay` call in this file generates the duplicate id `unrelated_${suffix}`, failing with an unrelated duplicate-key error that obscures the real failure. Restore in `try/finally`, or use `mockImplementationOnce` for a single poisoned UUID.</comment>
<file context>
@@ -0,0 +1,427 @@
+ .from(supportMetricDaily)
+ .where(eq(supportMetricDaily.teamId, ids.team))
+ .orderBy(asc(supportMetricDaily.id))
+ randomUuidMock.fn.mockReturnValue(`unrelated_${suffix}`)
+ await expect(recomputeSupportVolumeDay({ teamId: ids.team, date, timezone })).rejects.toThrow()
+ randomUuidMock.fn.mockImplementation(randomUuidMock.generate)
</file context>
| describe('database connection TLS configuration', () => { | ||
| it('requires verified TLS for the Neon pull-request test database', () => { | ||
| const workflow = readFileSync(new URL('../.github/workflows/neon.yml', import.meta.url), 'utf8') | ||
| const playwrightJob = workflow.split(' playwright_e2e:')[1]?.split('\n delete_neon_branch:')[0] |
There was a problem hiding this comment.
P3: This guard isolates the playwright_e2e job with whitespace-exact string splits (' playwright_e2e:' / '\n delete_neon_branch:') of a YAML file. Renaming or reordering delete_neon_branch, or reformatting job indentation, silently degrades the split (the assertion then scans the rest of the file, or the test fails spuriously), so the TLS regression guard becomes brittle. Since the goal is only to prevent relaxing DATABASE_SSL_MODE in this workflow, assert on the whole file instead of parsing job boundaries by exact indentation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/database-connection-config.test.ts, line 29:
<comment>This guard isolates the `playwright_e2e` job with whitespace-exact string splits (`' playwright_e2e:'` / `'\n delete_neon_branch:'`) of a YAML file. Renaming or reordering `delete_neon_branch`, or reformatting job indentation, silently degrades the split (the assertion then scans the rest of the file, or the test fails spuriously), so the TLS regression guard becomes brittle. Since the goal is only to prevent relaxing `DATABASE_SSL_MODE` in this workflow, assert on the whole file instead of parsing job boundaries by exact indentation.</comment>
<file context>
@@ -0,0 +1,264 @@
+describe('database connection TLS configuration', () => {
+ it('requires verified TLS for the Neon pull-request test database', () => {
+ const workflow = readFileSync(new URL('../.github/workflows/neon.yml', import.meta.url), 'utf8')
+ const playwrightJob = workflow.split(' playwright_e2e:')[1]?.split('\n delete_neon_branch:')[0]
+
+ expect(playwrightJob).toMatch(/^\s+DATABASE_SSL_MODE:\s*verify-full\s*$/m)
</file context>
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
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="tests/e2e/support-permissions.spec.ts">
<violation number="1" location="tests/e2e/support-permissions.spec.ts:618">
P2: The listener-readiness guard keys on the wrong component. The init-script marker is set by any `window` listener for `veerify:active-team-changed`, and AppSidebar (layouts/dashboard.vue) registers exactly that event on mount — before `pages/support/index.vue` does, since its listener is added only after the async `initTeamContext()` (including the unmocked csat round trip) resolves. `waitForFunction` can therefore pass while the support index handler is still unattached, and the two `window.dispatchEvent` calls can be lost, leaving the serial suite hung on `await oldListReady` — the race the two stabilization commits were meant to close. Make readiness specific to the support index listener: have `mounted()` in pages/support/index.vue set its own data attribute after registering the handler, and have the test wait on that instead of patching `addEventListener`.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| ) | ||
|
|
||
| await page.reload({ waitUntil: 'domcontentloaded' }) | ||
| await page.waitForFunction(() => document.documentElement.dataset.supportActiveTeamListenerReady === 'true') |
There was a problem hiding this comment.
P2: The listener-readiness guard keys on the wrong component. The init-script marker is set by any window listener for veerify:active-team-changed, and AppSidebar (layouts/dashboard.vue) registers exactly that event on mount — before pages/support/index.vue does, since its listener is added only after the async initTeamContext() (including the unmocked csat round trip) resolves. waitForFunction can therefore pass while the support index handler is still unattached, and the two window.dispatchEvent calls can be lost, leaving the serial suite hung on await oldListReady — the race the two stabilization commits were meant to close. Make readiness specific to the support index listener: have mounted() in pages/support/index.vue set its own data attribute after registering the handler, and have the test wait on that instead of patching addEventListener.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/e2e/support-permissions.spec.ts, line 618:
<comment>The listener-readiness guard keys on the wrong component. The init-script marker is set by any `window` listener for `veerify:active-team-changed`, and AppSidebar (layouts/dashboard.vue) registers exactly that event on mount — before `pages/support/index.vue` does, since its listener is added only after the async `initTeamContext()` (including the unmocked csat round trip) resolves. `waitForFunction` can therefore pass while the support index handler is still unattached, and the two `window.dispatchEvent` calls can be lost, leaving the serial suite hung on `await oldListReady` — the race the two stabilization commits were meant to close. Make readiness specific to the support index listener: have `mounted()` in pages/support/index.vue set its own data attribute after registering the handler, and have the test wait on that instead of patching `addEventListener`.</comment>
<file context>
@@ -606,6 +614,9 @@ test.describe.serial('support permission-aware navigation', () => {
)
+ await page.reload({ waitUntil: 'domcontentloaded' })
+ await page.waitForFunction(() => document.documentElement.dataset.supportActiveTeamListenerReady === 'true')
+ await expect(page.getByTestId(`support-inbox-switch-${fixture.primaryInboxId}`)).toBeVisible()
activeTeamTransition = 1
</file context>
There was a problem hiding this comment.
1 existing issue remains and 6 new issues found across 78 files (changes from recent commits).
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="server/utils/inbound-threading.ts">
<violation number="1" location="server/utils/inbound-threading.ts:9">
P3: Custom agent: **Flag AI Slop and Fabricated Changes**
This comment repeats the same asymmetric-failure explanation twice. Remove the first duplicate sentences or fold the rationale into one occurrence so the comment is cohesive.</violation>
<violation number="2" location="server/utils/inbound-threading.ts:26">
P3: The `ThreadableMessage` comment is now a broken, duplicated sentence: "An `InboundMessage` satisfies this shape, so the call site in The inbound adapter satisfies this shape — …". Removing the doc reference left the clause "so the call site in" dangling, and the shape explanation is stated twice. Drop the leftover clause so the paragraph starts with "The inbound adapter satisfies this shape …".</violation>
</file>
<file name="server/utils/outbound-delivery.ts">
<violation number="1" location="server/utils/outbound-delivery.ts:289">
P3: This comment now reads as a broken sentence: "See the initial implementation rationale is retained in the support platform design notes." The mid-sentence doc rewrite dropped the original "landing note in `parallel-agents.md` for the full story" framing and left a non-parseable clause. Rejoin it as a continuation of "See the".</violation>
</file>
<file name="server/database/schema/support.ts">
<violation number="1" location="server/database/schema/support.ts:840">
P3: The comment edit removed the `See parallel-agents.md, "` opening of the quoted phrase but left its closing quote on the next line, so the comment now reads `the delivery webhook gets its own table", and delta D-35...` with a dangling `"` and a broken quote pair. Drop the stray quote (and the now-unneeded comma) so the comment reads as plain prose.</violation>
</file>
<file name="TODO.md">
<violation number="1" location="TODO.md:224">
P2: This block now contradicts the new 'Completed stage' label directly above it: it still says migration `0025` is 'One migration expected' and that `supportOutboundDelivery` / `supportDeliveryEvent` are 'never created; the schema is at `0024`'. `server/database/migrations/0025_amazing_gabe_jones.sql` already creates both tables (and both exist in `server/database/schema/support.ts`), so this text is stale plan prose that will mislead anyone following the stage record. Rewrite the block to state that 0025 shipped both tables.</violation>
<violation number="2" location="TODO.md:349">
P3: Dangling 'first' left over from the old 'Read `design.md` and `deltas.md` first' phrasing. The line now reads 'Completed stage; see ... `deltas.md` first.', and the Stage 05A rewrite already dropped the word. Remove it.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
|
||
| ## Support Platform — Stage 01: Contact identity | ||
|
|
||
| Completed stage; see `docs/plans/2026-08-11-support-platform/README.md`, `design.md`, and `deltas.md`. |
There was a problem hiding this comment.
P2: This block now contradicts the new 'Completed stage' label directly above it: it still says migration 0025 is 'One migration expected' and that supportOutboundDelivery / supportDeliveryEvent are 'never created; the schema is at 0024'. server/database/migrations/0025_amazing_gabe_jones.sql already creates both tables (and both exist in server/database/schema/support.ts), so this text is stale plan prose that will mislead anyone following the stage record. Rewrite the block to state that 0025 shipped both tables.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At TODO.md, line 224:
<comment>This block now contradicts the new 'Completed stage' label directly above it: it still says migration `0025` is 'One migration expected' and that `supportOutboundDelivery` / `supportDeliveryEvent` are 'never created; the schema is at `0024`'. `server/database/migrations/0025_amazing_gabe_jones.sql` already creates both tables (and both exist in `server/database/schema/support.ts`), so this text is stale plan prose that will mislead anyone following the stage record. Rewrite the block to state that 0025 shipped both tables.</comment>
<file context>
@@ -221,8 +221,8 @@ directory first. Infrastructure only — no support tables, endpoints, or UI in
-Plan: `docs/plans/2026-08-11-support-platform/stage-01-contacts.md`. Read `design.md` and `deltas.md`
-first. Integration branch is **`support-platform`**, not `main` (delta D-17).
+Completed stage; see `docs/plans/2026-08-11-support-platform/README.md`, `design.md`, and `deltas.md`.
+Integration branch is **`support-platform`**, not `main` (delta D-17).
</file context>
| * Threading resolution for inbound mail — deciding whether a message continues | ||
| * an existing conversation or starts a new one. | ||
| * | ||
| * A missed match splits one conversation into two, which an agent can merge, |
There was a problem hiding this comment.
P3: Custom agent: Flag AI Slop and Fabricated Changes
This comment repeats the same asymmetric-failure explanation twice. Remove the first duplicate sentences or fold the rationale into one occurrence so the comment is cohesive.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/utils/inbound-threading.ts, line 9:
<comment>This comment repeats the same asymmetric-failure explanation twice. Remove the first duplicate sentences or fold the rationale into one occurrence so the comment is cohesive.</comment>
<file context>
@@ -6,8 +6,9 @@ import { conversation, conversationMessage } from '~/server/database/schema/supp
*
- * `stage-03-inbound-email.md` calls this "the classic source of duplicate
- * tickets", and the failure is asymmetric: a missed match splits one
+ * A missed match splits one conversation into two, which an agent can merge,
+ * while a wrong match shows one customer another customer's correspondence.
+ * The failure is asymmetric: a missed match splits one
</file context>
| * deliveryStatus` past its initial insert-time value - `completeOutboundDelivery` | ||
| * and `failOutboundDelivery` only touched the outbox row. A message would sit | ||
| * at `'pending'` forever regardless of whether it actually sent. See the | ||
| * initial implementation rationale is retained in the support platform design notes. |
There was a problem hiding this comment.
P3: This comment now reads as a broken sentence: "See the initial implementation rationale is retained in the support platform design notes." The mid-sentence doc rewrite dropped the original "landing note in parallel-agents.md for the full story" framing and left a non-parseable clause. Rejoin it as a continuation of "See the".
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/utils/outbound-delivery.ts, line 289:
<comment>This comment now reads as a broken sentence: "See the initial implementation rationale is retained in the support platform design notes." The mid-sentence doc rewrite dropped the original "landing note in `parallel-agents.md` for the full story" framing and left a non-parseable clause. Rejoin it as a continuation of "See the".</comment>
<file context>
@@ -286,7 +286,7 @@ export async function claimNextOutboundDelivery(options: { now?: Date } = {}): P
* and `failOutboundDelivery` only touched the outbox row. A message would sit
* at `'pending'` forever regardless of whether it actually sent. See the
- * landing note in `parallel-agents.md` for the full story.
+ * initial implementation rationale is retained in the support platform design notes.
*/
async function applyDeliveryOutcome(input: {
</file context>
| * initial implementation rationale is retained in the support platform design notes. | |
| * initial implementation rationale in the support platform design notes. |
| // to catch. The delivery webhook gets its own | ||
| // table", and delta D-35 for why `messageId` below is nullable for the same |
There was a problem hiding this comment.
P3: The comment edit removed the See parallel-agents.md, " opening of the quoted phrase but left its closing quote on the next line, so the comment now reads the delivery webhook gets its own table", and delta D-35... with a dangling " and a broken quote pair. Drop the stray quote (and the now-unneeded comma) so the comment reads as plain prose.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/database/schema/support.ts, line 840:
<comment>The comment edit removed the `See parallel-agents.md, "` opening of the quoted phrase but left its closing quote on the next line, so the comment now reads `the delivery webhook gets its own table", and delta D-35...` with a dangling `"` and a broken quote pair. Drop the stray quote (and the now-unneeded comma) so the comment reads as plain prose.</comment>
<file context>
@@ -837,7 +837,7 @@ export const supportOutboundDelivery = pgTable(
// SpamComplaint). Sharing the key would silently swallow every event after
// the first - including the hard bounce that acceptance criterion 6 exists
-// to catch. See `parallel-agents.md`, "the delivery webhook gets its own
+// to catch. The delivery webhook gets its own
// table", and delta D-35 for why `messageId` below is nullable for the same
// reason `supportEmailEvent.inboxId` is.
</file context>
| // to catch. The delivery webhook gets its own | |
| // table", and delta D-35 for why `messageId` below is nullable for the same | |
| // to catch. The delivery webhook gets its own | |
| // table, and delta D-35 for why `messageId` below is nullable for the same |
| * **Deliberately structural rather than importing `InboundMessage`** from | ||
| * `server/services/support-channels/types.ts` (Agent 1's file). An | ||
| * `InboundMessage` satisfies this shape, so the call site in | ||
| * The inbound adapter satisfies this shape — but `server/utils/` does not take a |
There was a problem hiding this comment.
P3: The ThreadableMessage comment is now a broken, duplicated sentence: "An InboundMessage satisfies this shape, so the call site in The inbound adapter satisfies this shape — …". Removing the doc reference left the clause "so the call site in" dangling, and the shape explanation is stated twice. Drop the leftover clause so the paragraph starts with "The inbound adapter satisfies this shape …".
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/utils/inbound-threading.ts, line 26:
<comment>The `ThreadableMessage` comment is now a broken, duplicated sentence: "An `InboundMessage` satisfies this shape, so the call site in The inbound adapter satisfies this shape — …". Removing the doc reference left the clause "so the call site in" dangling, and the shape explanation is stated twice. Drop the leftover clause so the paragraph starts with "The inbound adapter satisfies this shape …".</comment>
<file context>
@@ -22,7 +23,7 @@ type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
* `server/services/support-channels/types.ts` (Agent 1's file). An
* `InboundMessage` satisfies this shape, so the call site in
- * `parallel-agents.md` compiles unchanged — but `server/utils/` does not take a
+ * The inbound adapter satisfies this shape — but `server/utils/` does not take a
* dependency on `server/services/support-channels/`, and these tests need only
* four fields rather than a whole normalized message. Flagged to Agent 1 rather
</file context>
|
|
||
| ## Support Platform — Stage 02: Inbox + conversation core | ||
|
|
||
| Completed stage; see `docs/plans/2026-08-11-support-platform/README.md`, `design.md`, and |
There was a problem hiding this comment.
P3: Dangling 'first' left over from the old 'Read design.md and deltas.md first' phrasing. The line now reads 'Completed stage; see ... deltas.md first.', and the Stage 05A rewrite already dropped the word. Remove it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At TODO.md, line 349:
<comment>Dangling 'first' left over from the old 'Read `design.md` and `deltas.md` first' phrasing. The line now reads 'Completed stage; see ... `deltas.md` first.', and the Stage 05A rewrite already dropped the word. Remove it.</comment>
<file context>
@@ -350,7 +346,7 @@ Plan: `docs/plans/2026-08-11-support-platform/stage-03-inbound-email.md`. Read `
## Support Platform — Stage 02: Inbox + conversation core
-Plan: `docs/plans/2026-08-11-support-platform/stage-02-conversation-core.md`. Read `design.md` and
+Completed stage; see `docs/plans/2026-08-11-support-platform/README.md`, `design.md`, and
`deltas.md` first. Integration branch is **`support-platform`**.
</file context>
Summary
Verification
yarn harness:verifyon1e78601: format, typecheck, 716 unit tests, lint (0 errors; existing warnings), Redis integration (6 passed), and Postgres integration (133 passed, 1 guarded realtime skip becauseDATABASE_URLwas unset).PLAYWRIGHT_FORCEunset, and no database connection configured.8fbf4ef: 119 passed, 4 skipped, 0 failed. The delayed team-switch regression also passed three consecutive local runs against an isolated disposable database.Readiness and deployment boundary
UNSTABLEwhile Vercel deployment fails: this project requires subdaily support-worker schedules, while the connected Vercel Hobby plan only accepts daily cron jobs. The schedule was not weakened because that would break processing cadence.