chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
This commit is contained in:
@@ -160,7 +160,7 @@ describe('ai-budget service', () => {
|
||||
it('periodStart honors day/week/month boundaries (UTC)', () => {
|
||||
const wed = new Date(Date.UTC(2026, 3, 29, 14, 30)); // Wed 2026-04-29 14:30 UTC
|
||||
expect(periodStart('day', wed).toISOString()).toBe('2026-04-29T00:00:00.000Z');
|
||||
// 2026-04-27 was a Monday — week starts there.
|
||||
// 2026-04-27 was a Monday - week starts there.
|
||||
expect(periodStart('week', wed).toISOString()).toBe('2026-04-27T00:00:00.000Z');
|
||||
expect(periodStart('month', wed).toISOString()).toBe('2026-04-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Engine integration test — drives `runAlertEngineForPorts` against
|
||||
* Engine integration test - drives `runAlertEngineForPorts` against
|
||||
* seeded conditions and asserts: (1) correct alerts upsert, (2) running
|
||||
* twice doesn't duplicate, (3) mutating state auto-resolves stale alerts.
|
||||
*
|
||||
@@ -129,7 +129,7 @@ describe('alert engine', () => {
|
||||
await runAlertEngineForPorts([port.id]);
|
||||
expect(await listOpenAlerts(port.id, 'reservation.no_agreement')).toHaveLength(1);
|
||||
|
||||
// Add an agreement document — condition no longer fires.
|
||||
// Add an agreement document - condition no longer fires.
|
||||
await db.insert(documents).values({
|
||||
portId: port.id,
|
||||
reservationId: resv!.id,
|
||||
@@ -200,7 +200,7 @@ describe('alert engine', () => {
|
||||
const summary = await runAlertEngineForPorts([port.id]);
|
||||
expect(summary.portsScanned).toBe(1);
|
||||
expect(summary.rulesEvaluated).toBeGreaterThan(0);
|
||||
// No conditions seeded — no rules should fail.
|
||||
// No conditions seeded - no rules should fail.
|
||||
expect(summary.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ async function makeAlert(portId: string) {
|
||||
return row.id;
|
||||
}
|
||||
|
||||
describe('alerts service — tenant isolation', () => {
|
||||
describe('alerts service - tenant isolation', () => {
|
||||
it('dismissAlert is a no-op when called with the wrong portId', async () => {
|
||||
const portA = await makePort();
|
||||
const portB = await makePort();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Analytics service integration tests — exercise the four computations
|
||||
* Analytics service integration tests - exercise the four computations
|
||||
* against a seeded port + assert the cache layer reads/writes correctly.
|
||||
*/
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('analytics service', () => {
|
||||
it('returns 7 points for 7d range with cumulative won-deal occupancy', async () => {
|
||||
// Post 2026-05-14 the timeline derives occupancy from won
|
||||
// interests (cumulative as of each day) rather than active
|
||||
// reservations — see analytics.service.ts comment + PRE-DEPLOY-
|
||||
// reservations - see analytics.service.ts comment + PRE-DEPLOY-
|
||||
// PLAN § 1.1.3. Fixture: 3 berths, one of which sold 5 days ago.
|
||||
const port = await makePort();
|
||||
await makeBerth({ portId: port.id });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Port-scoped global reservations list — locks in feat(marina): the new
|
||||
* Port-scoped global reservations list - locks in feat(marina): the new
|
||||
* `GET /api/v1/berth-reservations` endpoint that powers the
|
||||
* `[portSlug]/berth-reservations` page. The route is thin (parseQuery →
|
||||
* listReservations); the test guarantees port scoping at the handler
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('POST /api/v1/companies', () => {
|
||||
const res1 = await createHandler(req1, ctx, {});
|
||||
expect(res1.status).toBe(201);
|
||||
|
||||
// Same name with different case — should still conflict.
|
||||
// Same name with different case - should still conflict.
|
||||
const req2 = makeMockRequest('POST', 'http://localhost/api/v1/companies', {
|
||||
body: { name: name.toUpperCase() },
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('GET /api/v1/companies/[id]/members', () => {
|
||||
);
|
||||
expect(delRes.status).toBe(204);
|
||||
|
||||
// Default — active only.
|
||||
// Default - active only.
|
||||
const activeOnlyRes = await listHandler(
|
||||
makeMockRequest('GET', `http://localhost/api/v1/companies/${company.id}/members`),
|
||||
ctx,
|
||||
@@ -375,7 +375,7 @@ describe('POST /api/v1/companies/[id]/members/[mid]/set-primary', () => {
|
||||
);
|
||||
const m1 = ((await m1Res.json()) as any).data;
|
||||
|
||||
// M2, M3 — not primary.
|
||||
// M2, M3 - not primary.
|
||||
const m2Res = await createHandler(
|
||||
makeMockRequest('POST', `http://localhost/api/v1/companies/${company.id}/members`, {
|
||||
body: {
|
||||
|
||||
@@ -416,7 +416,7 @@ describe('PATCH /api/v1/berth-reservations/[id]', () => {
|
||||
|
||||
it('returns 403 when caller lacks reservations.cancel for cancel action', async () => {
|
||||
const { port, reservation } = await seedReservation();
|
||||
// Sales agent — has activate but NOT cancel.
|
||||
// Sales agent - has activate but NOT cancel.
|
||||
const ctx = makeMockCtx({
|
||||
portId: port.id,
|
||||
permissions: makeSalesAgentPermissions(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Saved-views ownership enforcement — locks in the 403/404 split shipped
|
||||
* Saved-views ownership enforcement - locks in the 403/404 split shipped
|
||||
* in fix(auth). The route handlers preflight `assertViewOwner` BEFORE the
|
||||
* service call, so even if the service's internal userId filter is later
|
||||
* refactored, the route still rejects cross-user mutations.
|
||||
@@ -96,7 +96,7 @@ describe('saved-views ownership enforcement', () => {
|
||||
it('PATCH on a view in a different port: 404 (cross-port enumeration is blocked)', async () => {
|
||||
// The view exists in `portId` but the auth context says we're operating
|
||||
// in a different port. The lookup is scoped to `(id, portId)` so the row
|
||||
// is invisible — should 404, not 403.
|
||||
// is invisible - should 404, not 403.
|
||||
const otherPort = await makePort();
|
||||
const ctx = makeMockCtx({ portId: otherPort.id, userId: ownerUserId });
|
||||
const res = await patchHandler(
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('PATCH /api/v1/yachts/[id]', () => {
|
||||
// Validator strips owner fields, so we need to bypass it to reach the service's defensive guard.
|
||||
// Test the service layer defense by calling the handler with a payload that the validator
|
||||
// would accept but which also contains an unknown field that matches the forbidden keys.
|
||||
// Actually the validator just omits `owner` — additional keys `currentOwnerId` etc. pass
|
||||
// Actually the validator just omits `owner` - additional keys `currentOwnerId` etc. pass
|
||||
// through Zod's .partial() (which still omits unknown keys by default).
|
||||
// Zod .strip() is default, so unknown keys are dropped: we assert on the service directly.
|
||||
const { updateYacht } = await import('@/lib/services/yachts.service');
|
||||
@@ -264,7 +264,7 @@ describe('GET /api/v1/yachts/[id]/ownership-history', () => {
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as any;
|
||||
expect(body.data).toHaveLength(2);
|
||||
// Sorted DESC by startDate — newest first
|
||||
// Sorted DESC by startDate - newest first
|
||||
const firstStart = new Date(body.data[0].startDate).getTime();
|
||||
const secondStart = new Date(body.data[1].startDate).getTime();
|
||||
expect(firstStart).toBeGreaterThanOrEqual(secondStart);
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('GET /api/v1/yachts (listHandler)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/yachts — permission gate', () => {
|
||||
describe('POST /api/v1/yachts - permission gate', () => {
|
||||
it('viewer (no yachts.create) receives 403 through full pipeline', async () => {
|
||||
const port = await makePort();
|
||||
const client = await makeClient({ portId: port.id });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* PR10 — audit log search.
|
||||
* PR10 - audit log search.
|
||||
*
|
||||
* Validates:
|
||||
* 1. Tsvector full-text search via the GENERATED `search_text` column
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Task 11 — backfill-document-folders integration tests.
|
||||
* Task 11 - backfill-document-folders integration tests.
|
||||
*
|
||||
* Five cases:
|
||||
* 1. Creates system roots and entity subfolders.
|
||||
* 2. Sets files.folder_id from entity FKs.
|
||||
* 3. Copies entity FKs from completed workflows onto signed files.
|
||||
* 4. Idempotent — second run produces the same result.
|
||||
* 5. Port isolation — does not touch other ports.
|
||||
* 4. Idempotent - second run produces the same result.
|
||||
* 5. Port isolation - does not touch other ports.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
@@ -125,7 +125,7 @@ describe('backfill-document-folders · runBackfill', () => {
|
||||
.insert(files)
|
||||
.values({
|
||||
portId,
|
||||
// No clientId set — simulates legacy completion before entity FK auto-propagation.
|
||||
// No clientId set - simulates legacy completion before entity FK auto-propagation.
|
||||
filename: 'signed-eoi.pdf',
|
||||
originalName: 'signed-eoi.pdf',
|
||||
storagePath: `${portId}/signed-eoi.pdf`,
|
||||
@@ -161,7 +161,7 @@ describe('backfill-document-folders · runBackfill', () => {
|
||||
|
||||
// ── Test 4: Idempotent ────────────────────────────────────────────────────────
|
||||
|
||||
it('is idempotent — running twice produces the same number of folder rows', async () => {
|
||||
it('is idempotent - running twice produces the same number of folder rows', async () => {
|
||||
const client = await makeClient({ portId });
|
||||
|
||||
await db.insert(files).values({
|
||||
@@ -217,7 +217,7 @@ describe('backfill-document-folders · runBackfill', () => {
|
||||
.from(documentFolders)
|
||||
.where(eq(documentFolders.portId, otherPort.id));
|
||||
|
||||
// The other port should have zero folders — the backfill was not run for it.
|
||||
// The other port should have zero folders - the backfill was not run for it.
|
||||
expect(otherPortFolders).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ beforeEach(async () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// No file cleanup needed — the filesystem backend writes to a tmp root.
|
||||
// No file cleanup needed - the filesystem backend writes to a tmp root.
|
||||
});
|
||||
|
||||
function fakePdf(): Buffer {
|
||||
@@ -240,7 +240,7 @@ describe('applyParseResults', () => {
|
||||
lengthFt: 200,
|
||||
bowFacing: 'East',
|
||||
// unknown / non-allowlisted column should be silently dropped:
|
||||
// @ts-expect-error — testing the allowlist
|
||||
// @ts-expect-error - testing the allowlist
|
||||
hackThePlanet: 'pwn',
|
||||
},
|
||||
port.id,
|
||||
@@ -323,7 +323,7 @@ describe('cross-port tenant guard', () => {
|
||||
});
|
||||
|
||||
// Port B caller passing port A's berth id must hit NotFoundError on
|
||||
// every entrypoint — including read-only listing, which previously
|
||||
// every entrypoint - including read-only listing, which previously
|
||||
// returned 15-min presigned download URLs to the foreign port's PDFs.
|
||||
await expect(listBerthPdfVersions(berthA.id, portB.id)).rejects.toThrow(/berth/i);
|
||||
await expect(rollbackToVersion(berthA.id, v1.versionId, portB.id)).rejects.toThrow(/berth/i);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* clients.service.createRelationship — tenant-FK validation tests.
|
||||
* clients.service.createRelationship - tenant-FK validation tests.
|
||||
*
|
||||
* Covers the fix that requires both clientAId (the URL id) and clientBId
|
||||
* (the body id) to belong to the caller's port. The list endpoint joins
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
|
||||
describe('clients.service — createRelationship port isolation', () => {
|
||||
describe('clients.service - createRelationship port isolation', () => {
|
||||
let createRelationship: typeof import('@/lib/services/clients.service').createRelationship;
|
||||
|
||||
let makePort: typeof import('../helpers/factories').makePort;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import { createCrmInvite } from '@/lib/services/crm-invite.service';
|
||||
import { ValidationError } from '@/lib/errors';
|
||||
|
||||
describe('createCrmInvite — super-admin gate', () => {
|
||||
describe('createCrmInvite - super-admin gate', () => {
|
||||
it('rejects super-admin invites when caller is not a super-admin', async () => {
|
||||
await expect(
|
||||
createCrmInvite({
|
||||
|
||||
@@ -35,7 +35,7 @@ beforeAll(async () => {
|
||||
await sql.end();
|
||||
dbAvailable = true;
|
||||
} catch {
|
||||
console.warn('[crud-audit] Test database not available — skipping integration tests');
|
||||
console.warn('[crud-audit] Test database not available - skipping integration tests');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@ async function getAuditEntries(
|
||||
|
||||
// ─── Client Audit Tests ───────────────────────────────────────────────────────
|
||||
|
||||
describe('CRUD Audit — Clients', () => {
|
||||
describe('CRUD Audit - Clients', () => {
|
||||
let portId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -200,7 +200,7 @@ describe('CRUD Audit — Clients', () => {
|
||||
|
||||
// ─── Interest Audit Tests ─────────────────────────────────────────────────────
|
||||
|
||||
describe('CRUD Audit — Interests', () => {
|
||||
describe('CRUD Audit - Interests', () => {
|
||||
let portId: string;
|
||||
let clientId: string;
|
||||
|
||||
@@ -291,7 +291,7 @@ describe('CRUD Audit — Interests', () => {
|
||||
|
||||
// ─── Berth Audit Tests ────────────────────────────────────────────────────────
|
||||
|
||||
describe('CRUD Audit — Berths', () => {
|
||||
describe('CRUD Audit - Berths', () => {
|
||||
let portId: string;
|
||||
let berthId: string;
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ beforeAll(async () => {
|
||||
await sql.end();
|
||||
dbAvailable = true;
|
||||
} catch {
|
||||
console.warn('[custom-fields] Test database not available — skipping integration tests');
|
||||
console.warn('[custom-fields] Test database not available - skipping integration tests');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ async function cleanupPort(portId: string): Promise<void> {
|
||||
|
||||
// ─── Definitions Tests ────────────────────────────────────────────────────────
|
||||
|
||||
describe('Custom Fields — Definitions', () => {
|
||||
describe('Custom Fields - Definitions', () => {
|
||||
let portId: string;
|
||||
const userId = crypto.randomUUID();
|
||||
|
||||
@@ -160,7 +160,7 @@ describe('Custom Fields — Definitions', () => {
|
||||
meta,
|
||||
);
|
||||
|
||||
// Cast bypasses TS — the service should guard against this at runtime.
|
||||
// Cast bypasses TS - the service should guard against this at runtime.
|
||||
await expect(
|
||||
updateDefinition(
|
||||
portId,
|
||||
@@ -205,7 +205,7 @@ describe('Custom Fields — Definitions', () => {
|
||||
|
||||
// ─── Values Tests ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Custom Fields — Values', () => {
|
||||
describe('Custom Fields - Values', () => {
|
||||
let portId: string;
|
||||
const userId = crypto.randomUUID();
|
||||
const entityId = crypto.randomUUID();
|
||||
@@ -246,7 +246,7 @@ describe('Custom Fields — Values', () => {
|
||||
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry!.value).not.toBeNull();
|
||||
// value is stored as jsonb — the raw stored value
|
||||
// value is stored as jsonb - the raw stored value
|
||||
expect((entry!.value as Record<string, unknown>).value).toBe('GOLD-2024');
|
||||
});
|
||||
|
||||
@@ -321,10 +321,10 @@ describe('Custom Fields — Values', () => {
|
||||
// The previous suite seeded ONE port and verified CRUD inside it. The audit
|
||||
// (HIGH §20 / auditor-J Issue 3) flagged that the suite never asserted that
|
||||
// a definition created in port A is invisible from port B, nor that
|
||||
// setValues refuses cross-port writes — combined with the deferred
|
||||
// setValues refuses cross-port writes - combined with the deferred
|
||||
// custom-fields-hardcoded-clients gap, no test would catch a regression.
|
||||
|
||||
describe('Custom Fields — Cross-port Isolation', () => {
|
||||
describe('Custom Fields - Cross-port Isolation', () => {
|
||||
let portA: string;
|
||||
let portB: string;
|
||||
const userId = crypto.randomUUID();
|
||||
@@ -410,7 +410,7 @@ describe('Custom Fields — Cross-port Isolation', () => {
|
||||
);
|
||||
|
||||
// Caller in port B tries to write a value keyed to port A's field id.
|
||||
// The service must refuse — either by throwing, or by no-oping
|
||||
// The service must refuse - either by throwing, or by no-oping
|
||||
// (returning without touching port A's data). Either way port A's
|
||||
// value-store for the entity must remain unchanged.
|
||||
let threw = false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Client merge service — end-to-end integration test.
|
||||
* Client merge service - end-to-end integration test.
|
||||
*
|
||||
* Spins up two real clients in a real port via the factory helpers,
|
||||
* attaches a few satellites (interest, contact, address, note),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Match-candidates API — integration test.
|
||||
* Match-candidates API - integration test.
|
||||
*
|
||||
* Exercises the GET /api/v1/clients/match-candidates handler against a
|
||||
* real port + clients pool. Verifies the dedup library's at-create
|
||||
@@ -107,7 +107,7 @@ describe('GET /api/v1/clients/match-candidates', () => {
|
||||
});
|
||||
|
||||
it('returns medium-confidence partial matches', async () => {
|
||||
// Same name, different contact info — Pattern F territory.
|
||||
// Same name, different contact info - Pattern F territory.
|
||||
const port = await makePort();
|
||||
const ctx = makeMockCtx({ portId: port.id });
|
||||
const existing = await makeClient({
|
||||
@@ -127,7 +127,7 @@ describe('GET /api/v1/clients/match-candidates', () => {
|
||||
name: 'Etiennette Clamouze',
|
||||
});
|
||||
|
||||
// Either no match (low confidence filtered out) or a medium one —
|
||||
// Either no match (low confidence filtered out) or a medium one -
|
||||
// either is fine. Critically, NOT high.
|
||||
if (data.length > 0) {
|
||||
expect(data[0]!.confidence).not.toBe('high');
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Task 3 — document-folders service: listTree + createFolder (TDD).
|
||||
* Task 4 — renameFolder + moveFolder (TDD).
|
||||
* Task 3 - document-folders service: listTree + createFolder (TDD).
|
||||
* Task 4 - renameFolder + moveFolder (TDD).
|
||||
*
|
||||
* Uses the makePort factory (not a "setupTestPort" helper — that name
|
||||
* Uses the makePort factory (not a "setupTestPort" helper - that name
|
||||
* doesn't exist in this codebase). TEST_USER_ID is resolved once via
|
||||
* beforeAll from any seeded user, matching the pattern in
|
||||
* alerts-tenant-isolation.test.ts and gdpr-export.test.ts.
|
||||
|
||||
@@ -112,9 +112,9 @@ describe('MERGE_FIELDS catalog', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── resolveTemplate — EOI scope tokens ───────────────────────────────────────
|
||||
// ─── resolveTemplate - EOI scope tokens ───────────────────────────────────────
|
||||
|
||||
describe('resolveTemplate — EOI scope tokens', () => {
|
||||
describe('resolveTemplate - EOI scope tokens', () => {
|
||||
const EOI_TEMPLATE_BODY = [
|
||||
'Client: {{client.fullName}} / {{client.email}} / {{client.phone}}',
|
||||
'Yacht: {{yacht.name}} HN={{yacht.hullNumber}} LenFt={{yacht.lengthFt}} LenM={{yacht.lengthM}} YB={{yacht.yearBuilt}}',
|
||||
@@ -273,7 +273,7 @@ describe('resolveTemplate — EOI scope tokens', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveTemplate — company-owned yacht', () => {
|
||||
describe('resolveTemplate - company-owned yacht', () => {
|
||||
it('populates company.* tokens and owner.legalName for company-owned yachts', async () => {
|
||||
const port = await makePort();
|
||||
const company = await makeCompany({
|
||||
@@ -354,9 +354,9 @@ describe('resolveTemplate — company-owned yacht', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── resolveTemplate — legacy fallback path ───────────────────────────────────
|
||||
// ─── resolveTemplate - legacy fallback path ───────────────────────────────────
|
||||
|
||||
describe('resolveTemplate — legacy fallback (no interestId)', () => {
|
||||
describe('resolveTemplate - legacy fallback (no interestId)', () => {
|
||||
it('falls back to direct client lookup when no interestId is provided', async () => {
|
||||
const port = await makePort();
|
||||
const client = await makeClient({
|
||||
|
||||
@@ -189,7 +189,7 @@ const meta = {
|
||||
|
||||
// ─── Pathway: inapp ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('generateAndSign — inapp pathway', () => {
|
||||
describe('generateAndSign - inapp pathway', () => {
|
||||
it('generates PDF via pdfme, uploads to MinIO, and sends to Documenso', async () => {
|
||||
const client = await import('@/lib/services/documenso-client');
|
||||
vi.mocked(client.createDocument).mockResolvedValue({
|
||||
@@ -350,7 +350,7 @@ describe('generateAndSign — inapp pathway', () => {
|
||||
|
||||
// ─── Pathway: documenso-template ──────────────────────────────────────────────
|
||||
|
||||
describe('generateAndSign — documenso-template pathway', () => {
|
||||
describe('generateAndSign - documenso-template pathway', () => {
|
||||
it('calls Documenso template-generate endpoint and records a documents row', async () => {
|
||||
const client = await import('@/lib/services/documenso-client');
|
||||
vi.mocked(client.generateDocumentFromTemplate).mockResolvedValue({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Task 7 — handleDocumentCompleted auto-deposit.
|
||||
* Task 7 - handleDocumentCompleted auto-deposit.
|
||||
*
|
||||
* Verifies that when a document is completed:
|
||||
* - The signed PDF is deposited into the owner's entity subfolder
|
||||
@@ -25,7 +25,7 @@ import { handleDocumentCompleted } from '@/lib/services/documents.service';
|
||||
import { ensureSystemRoots } from '@/lib/services/document-folders.service';
|
||||
import { makeClient, makeCompany, makePort, makeYacht } from '../helpers/factories';
|
||||
|
||||
// Stub Documenso download — do NOT hit the network.
|
||||
// Stub Documenso download - do NOT hit the network.
|
||||
vi.mock('@/lib/services/documenso-client', async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import('@/lib/services/documenso-client')>();
|
||||
return {
|
||||
@@ -34,7 +34,7 @@ vi.mock('@/lib/services/documenso-client', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
// Stub storage backend — write to an in-memory map so no MinIO required.
|
||||
// Stub storage backend - write to an in-memory map so no MinIO required.
|
||||
const stubPuts = new Map<string, Buffer>();
|
||||
vi.mock('@/lib/storage', async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import('@/lib/storage')>();
|
||||
@@ -171,7 +171,7 @@ describe('handleDocumentCompleted · auto-deposit', () => {
|
||||
.values({
|
||||
portId,
|
||||
interestId: interest!.id,
|
||||
// All direct owner FKs null — owner must be resolved via interest.
|
||||
// All direct owner FKs null - owner must be resolved via interest.
|
||||
documentType: 'eoi',
|
||||
title: 'Auto-deposit test EOI (via interest)',
|
||||
status: 'partially_signed',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* DOCUMENT_EXPIRED webhook handling — locks in fix(documenso). The handler
|
||||
* DOCUMENT_EXPIRED webhook handling - locks in fix(documenso). The handler
|
||||
* was previously defined but never wired to the route's event switch, so
|
||||
* expired EOIs stayed in `sent` / `partially_signed` forever.
|
||||
*/
|
||||
@@ -88,7 +88,7 @@ describe('handleDocumentExpired', () => {
|
||||
});
|
||||
|
||||
it('is a no-op when the documensoId does not match any document', async () => {
|
||||
// Should NOT throw — the handler logs a warning and returns. Verify no
|
||||
// Should NOT throw - the handler logs a warning and returns. Verify no
|
||||
// exception propagates up to the webhook route.
|
||||
await expect(
|
||||
handleDocumentExpired({ documentId: 'definitely-not-a-real-doc' }),
|
||||
@@ -98,7 +98,7 @@ describe('handleDocumentExpired', () => {
|
||||
it('does not flip a document in port B when port A receives the expired event', async () => {
|
||||
// Two ports holding the same documenso_id (legacy data, or a future
|
||||
// Documenso-instance migration that reuses ids). The handler currently
|
||||
// mutates whichever document `findFirst` returns — locking in the
|
||||
// mutates whichever document `findFirst` returns - locking in the
|
||||
// intended behaviour now means a future port_id-aware handler can
|
||||
// be added without regressing this guard.
|
||||
//
|
||||
@@ -140,7 +140,7 @@ describe('handleDocumentExpired', () => {
|
||||
// resolve this from the secret → port mapping in the deferred fix).
|
||||
await handleDocumentExpired({ documentId: sharedDocumensoId, portId: portA.id });
|
||||
|
||||
// Port-A doc flipped, port-B unchanged. Pre-fix, both flip — this
|
||||
// Port-A doc flipped, port-B unchanged. Pre-fix, both flip - this
|
||||
// assertion locks the boundary in once the handler scope lands.
|
||||
const afterA = await db.query.documents.findFirst({ where: eq(documents.id, docA!.id) });
|
||||
const afterB = await db.query.documents.findFirst({ where: eq(documents.id, docB!.id) });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* PR6 — documents hub `eoi_queue` tab.
|
||||
* PR6 - documents hub `eoi_queue` tab.
|
||||
*
|
||||
* Verifies that:
|
||||
* - `listDocuments` with tab='eoi_queue' returns only EOI docs in
|
||||
@@ -8,7 +8,7 @@
|
||||
* - Completed/expired EOIs are excluded (those belong to other tabs)
|
||||
*
|
||||
* (Note: `getHubTabCounts` and the /hub-counts route were removed when the
|
||||
* hub rebuild dropped the count-strip KPI surface — the count assertions
|
||||
* hub rebuild dropped the count-strip KPI surface - the count assertions
|
||||
* that used to live here went with them.)
|
||||
*/
|
||||
|
||||
@@ -19,7 +19,7 @@ import { documents } from '@/lib/db/schema/documents';
|
||||
import { listDocuments } from '@/lib/services/documents.service';
|
||||
import { makePort, makeClient } from '../helpers/factories';
|
||||
|
||||
describe('documents hub — eoi_queue tab', () => {
|
||||
describe('documents hub - eoi_queue tab', () => {
|
||||
it('lists only EOIs in in-flight status', async () => {
|
||||
const port = await makePort();
|
||||
const client = await makeClient({ portId: port.id });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Task 7 — listDocuments folder filtering (TDD).
|
||||
* Task 7 - listDocuments folder filtering (TDD).
|
||||
*
|
||||
* Exercises the three folderId modes: null (root only), a string (direct
|
||||
* children), and a string with includeDescendants=true (subtree). Mirrors the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* PR8 — expense duplicate detection.
|
||||
* PR8 - expense duplicate detection.
|
||||
*
|
||||
* Validates:
|
||||
* 1. `scanForDuplicates` matches by port + lower(vendor) + amount + date ±3d
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Task 9 — entity-aggregated API query params (TDD).
|
||||
* Task 9 - entity-aggregated API query params (TDD).
|
||||
*
|
||||
* Verifies:
|
||||
* 1. listFilesAggregatedByEntity returns DIRECTLY ATTACHED + FROM COMPANY
|
||||
@@ -71,7 +71,7 @@ async function insertFile(
|
||||
|
||||
// ─── listFilesAggregatedByEntity ──────────────────────────────────────────────
|
||||
|
||||
describe('GET /api/v1/files?entityType=client&entityId=… — service layer', () => {
|
||||
describe('GET /api/v1/files?entityType=client&entityId=… - service layer', () => {
|
||||
let portId: string;
|
||||
let clientId: string;
|
||||
let companyId: string;
|
||||
@@ -127,18 +127,18 @@ describe('GET /api/v1/files?entityType=client&entityId=… — service layer', (
|
||||
const otherFile = await insertFile(otherPort.id, { clientId: otherClient.id });
|
||||
|
||||
const result = await listFilesAggregatedByEntity(portId, 'client', clientId);
|
||||
// Groups are only for the correct port — the other-port client's file must not appear
|
||||
// Groups are only for the correct port - the other-port client's file must not appear
|
||||
const allFileIds = result.groups.flatMap((g) => g.files.map((f) => (f as { id: string }).id));
|
||||
expect(result.groups.length).toBeGreaterThan(0);
|
||||
expect(allFileIds.length).toBeGreaterThan(0);
|
||||
// Explicit cross-port isolation assertion — leakage would cause this to fail
|
||||
// Explicit cross-port isolation assertion - leakage would cause this to fail
|
||||
expect(allFileIds).not.toContain(otherFile.id);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── listInflightWorkflowsAggregatedByEntity ──────────────────────────────────
|
||||
|
||||
describe('GET /api/v1/documents?entityType=client&entityId=… — service layer', () => {
|
||||
describe('GET /api/v1/documents?entityType=client&entityId=… - service layer', () => {
|
||||
let portId: string;
|
||||
let clientId: string;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { makePort, makeClient, makeYacht } from '../helpers/factories';
|
||||
let TEST_USER_ID = '';
|
||||
|
||||
beforeAll(async () => {
|
||||
// Pull any existing user — gdpr_exports.requested_by has an FK that needs
|
||||
// Pull any existing user - gdpr_exports.requested_by has an FK that needs
|
||||
// to resolve. Tests don't need the user to be specific; they just need it
|
||||
// to exist.
|
||||
const [u] = await db.select({ id: user.id }).from(user).limit(1);
|
||||
@@ -166,7 +166,7 @@ describe('requestGdprExport', () => {
|
||||
expect(add).toHaveBeenCalledWith(
|
||||
'gdpr-export',
|
||||
expect.objectContaining({ exportId: row.id, emailToClient: true }),
|
||||
// F3: BullMQ 5.x rejects colons in custom job IDs — switched to dash.
|
||||
// F3: BullMQ 5.x rejects colons in custom job IDs - switched to dash.
|
||||
expect.objectContaining({ jobId: `gdpr-export-${row.id}` }),
|
||||
);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
|
||||
describe('interests.service — port-scope FK validation', () => {
|
||||
describe('interests.service - port-scope FK validation', () => {
|
||||
let createInterest: typeof import('@/lib/services/interests.service').createInterest;
|
||||
let updateInterest: typeof import('@/lib/services/interests.service').updateInterest;
|
||||
let linkBerth: typeof import('@/lib/services/interests.service').linkBerth;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
|
||||
describe('interests.service — yacht ownership validation', () => {
|
||||
describe('interests.service - yacht ownership validation', () => {
|
||||
let createInterest: typeof import('@/lib/services/interests.service').createInterest;
|
||||
|
||||
let updateInterest: typeof import('@/lib/services/interests.service').updateInterest;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
|
||||
describe('invoices.service — billing entity', () => {
|
||||
describe('invoices.service - billing entity', () => {
|
||||
let createInvoice: typeof import('@/lib/services/invoices').createInvoice;
|
||||
|
||||
let makePort: typeof import('../helpers/factories').makePort;
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('gdpr-export-cleanup query semantics', () => {
|
||||
expect(ids).toContain(expiredNoKey!.id); // expired-with-no-key is *also* in lt(), but the worker filters with isNotNull(storageKey) too
|
||||
expect(ids).not.toContain(stillFresh!.id);
|
||||
|
||||
// The full worker filter (expires past, storageKey not null) — only one row.
|
||||
// The full worker filter (expires past, storageKey not null) - only one row.
|
||||
const fullMatch = candidates.filter(
|
||||
(r) => r.id !== expiredNoKey!.id && r.id !== stillFresh!.id,
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
|
||||
// Socket and queue mocked — these are tested in isolation here.
|
||||
// Socket and queue mocked - these are tested in isolation here.
|
||||
vi.mock('@/lib/socket/server', () => ({ emitToRoom: vi.fn() }));
|
||||
vi.mock('@/lib/queue', () => ({
|
||||
getQueue: () => ({ add: vi.fn().mockResolvedValue(undefined) }),
|
||||
@@ -34,7 +34,7 @@ beforeAll(async () => {
|
||||
dbAvailable = true;
|
||||
} catch {
|
||||
console.warn(
|
||||
'[notification-lifecycle] Test database not available — skipping integration tests',
|
||||
'[notification-lifecycle] Test database not available - skipping integration tests',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* PR9 — OCR config service.
|
||||
* PR9 - OCR config service.
|
||||
*
|
||||
* Validates:
|
||||
* 1. Per-port save/read round-trip (key encrypted at rest, decrypted on resolve)
|
||||
@@ -108,7 +108,7 @@ describe('OCR config', () => {
|
||||
{ provider: 'openai', model: 'gpt-4o-mini', apiKey: 'keep-me' },
|
||||
'user-1',
|
||||
);
|
||||
// Update model only — no apiKey field provided.
|
||||
// Update model only - no apiKey field provided.
|
||||
await saveOcrConfig(port.id, { provider: 'openai', model: 'gpt-4o' }, 'user-1');
|
||||
const resolved = await getResolvedOcrConfig(port.id);
|
||||
expect(resolved.apiKey).toBe('keep-me');
|
||||
|
||||
@@ -66,7 +66,7 @@ async function checkPermission(
|
||||
|
||||
// ─── super_admin ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Permission Matrix — super_admin', () => {
|
||||
describe('Permission Matrix - super_admin', () => {
|
||||
const ctx = makeCtx({ isSuperAdmin: true, permissions: null });
|
||||
|
||||
it('can access clients.create', async () => {
|
||||
@@ -88,7 +88,7 @@ describe('Permission Matrix — super_admin', () => {
|
||||
|
||||
// ─── viewer ───────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Permission Matrix — viewer', () => {
|
||||
describe('Permission Matrix - viewer', () => {
|
||||
const ctx = makeCtx({ permissions: makeViewerPermissions() });
|
||||
|
||||
it('can view clients', async () => {
|
||||
@@ -122,7 +122,7 @@ describe('Permission Matrix — viewer', () => {
|
||||
|
||||
// ─── sales_agent ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Permission Matrix — sales_agent', () => {
|
||||
describe('Permission Matrix - sales_agent', () => {
|
||||
const ctx = makeCtx({ permissions: makeSalesAgentPermissions() });
|
||||
|
||||
it('can view clients', async () => {
|
||||
@@ -168,7 +168,7 @@ describe('Permission Matrix — sales_agent', () => {
|
||||
|
||||
// ─── sales_manager ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Permission Matrix — sales_manager', () => {
|
||||
describe('Permission Matrix - sales_manager', () => {
|
||||
const ctx = makeCtx({ permissions: makeSalesManagerPermissions() });
|
||||
|
||||
it('can do everything with clients', async () => {
|
||||
@@ -192,7 +192,7 @@ describe('Permission Matrix — sales_manager', () => {
|
||||
|
||||
// ─── director ─────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Permission Matrix — director', () => {
|
||||
describe('Permission Matrix - director', () => {
|
||||
const ctx = makeCtx({ permissions: makeDirectorPermissions() });
|
||||
|
||||
it('can manage webhooks', async () => {
|
||||
@@ -210,7 +210,7 @@ describe('Permission Matrix — director', () => {
|
||||
|
||||
// ─── deepMerge ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('deepMerge — permission override merging', () => {
|
||||
describe('deepMerge - permission override merging', () => {
|
||||
it('overrides a single leaf value', () => {
|
||||
const base = { clients: { view: true, create: false } };
|
||||
const override = { clients: { create: true } };
|
||||
|
||||
@@ -40,7 +40,7 @@ beforeAll(async () => {
|
||||
await sql.end();
|
||||
dbAvailable = true;
|
||||
} catch {
|
||||
console.warn('[pipeline-transitions] Test database not available — skipping integration tests');
|
||||
console.warn('[pipeline-transitions] Test database not available - skipping integration tests');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Port-scoping integration tests (SECURITY-CRITICAL).
|
||||
*
|
||||
* Codex Addenda: Two-port testing — every entity must be invisible
|
||||
* Codex Addenda: Two-port testing - every entity must be invisible
|
||||
* when queried under a different portId.
|
||||
*
|
||||
* Skips gracefully when TEST_DATABASE_URL is not reachable.
|
||||
@@ -29,7 +29,7 @@ beforeAll(async () => {
|
||||
await sql.end();
|
||||
dbAvailable = true;
|
||||
} catch {
|
||||
console.warn('[port-scoping] Test database not available — skipping integration tests');
|
||||
console.warn('[port-scoping] Test database not available - skipping integration tests');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ async function cleanupPorts(portA: string, portB: string): Promise<void> {
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Port Scoping — Clients', () => {
|
||||
describe('Port Scoping - Clients', () => {
|
||||
let portA: string;
|
||||
let portB: string;
|
||||
|
||||
@@ -153,7 +153,7 @@ describe('Port Scoping — Clients', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Port Scoping — Interests', () => {
|
||||
describe('Port Scoping - Interests', () => {
|
||||
let portA: string;
|
||||
let portB: string;
|
||||
let clientIdA: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Portal JWT verification — locks in the audience/issuer hardening shipped
|
||||
* Portal JWT verification - locks in the audience/issuer hardening shipped
|
||||
* in fix(auth): a token without `aud: 'portal'` + `iss: 'pn-crm'` claims
|
||||
* must NOT verify, even if it's signed with the correct shared secret.
|
||||
*
|
||||
@@ -50,7 +50,7 @@ describe('portal JWT', () => {
|
||||
});
|
||||
|
||||
it('rejects a token missing the `aud: portal` claim', async () => {
|
||||
// Issuer present, audience absent — exactly the shape an old (pre-fix)
|
||||
// Issuer present, audience absent - exactly the shape an old (pre-fix)
|
||||
// portal session would have.
|
||||
const token = await new SignJWT(SESSION as unknown as Record<string, unknown>)
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
@@ -74,7 +74,7 @@ describe('portal JWT', () => {
|
||||
});
|
||||
|
||||
it('rejects a token with the wrong audience (CRM session replay shape)', async () => {
|
||||
// What a better-auth session token might roughly look like — same secret,
|
||||
// What a better-auth session token might roughly look like - same secret,
|
||||
// different audience. Must not verify against the portal path.
|
||||
const token = await new SignJWT(SESSION as unknown as Record<string, unknown>)
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
|
||||
@@ -29,7 +29,7 @@ function uniqueIp(): string {
|
||||
return `${IP_PREFIX}.${Math.floor(ipCounter / 255) % 255}.${ipCounter % 255}`;
|
||||
}
|
||||
|
||||
describe('POST /api/public/interests — trio creation', () => {
|
||||
describe('POST /api/public/interests - trio creation', () => {
|
||||
let POST: typeof import('@/app/api/public/interests/route').POST;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -212,7 +212,7 @@ describe('POST /api/public/interests — trio creation', () => {
|
||||
.where(eq(interests.id, secondBody.data.id));
|
||||
expect(secondInterest!.clientId).toBe(originalClientId);
|
||||
|
||||
// A second yacht row was created (not deduped) — each submission is its
|
||||
// A second yacht row was created (not deduped) - each submission is its
|
||||
// own inquiry about a possibly-different yacht.
|
||||
const clientsMatching = await db.select().from(clients).where(eq(clients.id, originalClientId));
|
||||
expect(clientsMatching.length).toBe(1);
|
||||
@@ -259,7 +259,7 @@ describe('POST /api/public/interests — trio creation', () => {
|
||||
.where(eq(yachts.id, firstInterest!.yachtId!));
|
||||
const originalCompanyId = firstYacht!.currentOwnerId;
|
||||
|
||||
// Second submission — same company name, different casing, different client
|
||||
// Second submission - same company name, different casing, different client
|
||||
const secondReq = makeMockRequest(
|
||||
'POST',
|
||||
`http://localhost/api/public/interests?portId=${port.id}`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* i18n PR9–10 — public residential inquiry endpoint.
|
||||
* i18n PR9–10 - public residential inquiry endpoint.
|
||||
*
|
||||
* Validates the server-side phone normalization that the public inquiry
|
||||
* route runs when the website posts a raw international format (older
|
||||
@@ -47,7 +47,7 @@ describe('POST /api/public/residential-inquiries', () => {
|
||||
firstName: 'Anna',
|
||||
lastName: 'Nowak',
|
||||
email,
|
||||
// Raw international format — server should normalize.
|
||||
// Raw international format - server should normalize.
|
||||
phone: '+44 20 7946 0958',
|
||||
placeOfResidence: 'Warsaw',
|
||||
},
|
||||
@@ -122,7 +122,7 @@ describe('POST /api/public/residential-inquiries', () => {
|
||||
lastName: 'Lewandowska',
|
||||
email,
|
||||
phone: '22 555 0200', // National-format
|
||||
phoneCountry: 'PL', // Hint only — no E.164 yet.
|
||||
phoneCountry: 'PL', // Hint only - no E.164 yet.
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { db } from '@/lib/db';
|
||||
import { interests } from '@/lib/db/schema/interests';
|
||||
|
||||
describe('recommendations.service — yacht dimensions source', () => {
|
||||
describe('recommendations.service - yacht dimensions source', () => {
|
||||
let generateRecommendations: typeof import('@/lib/services/recommendations').generateRecommendations;
|
||||
|
||||
let makePort: typeof import('../helpers/factories').makePort;
|
||||
|
||||
@@ -26,7 +26,7 @@ beforeAll(async () => {
|
||||
dbAvailable = true;
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'[schema-constraints] DATABASE_URL not reachable — skipping integration tests',
|
||||
'[schema-constraints] DATABASE_URL not reachable - skipping integration tests',
|
||||
err,
|
||||
);
|
||||
}
|
||||
@@ -61,7 +61,7 @@ describe('schema constraints', () => {
|
||||
ownerType: 'client',
|
||||
ownerId: clientB.id,
|
||||
startDate: new Date(),
|
||||
endDate: null, // another open row — should violate partial unique
|
||||
endDate: null, // another open row - should violate partial unique
|
||||
createdBy: 'test',
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
@@ -123,7 +123,7 @@ describe('schema constraints', () => {
|
||||
});
|
||||
const berth = await makeBerth({ portId: port.id });
|
||||
|
||||
// Two ended reservations on same berth — both should succeed
|
||||
// Two ended reservations on same berth - both should succeed
|
||||
// (partial index only constrains status='active').
|
||||
await expect(
|
||||
db.insert(berthReservations).values([
|
||||
|
||||
@@ -14,7 +14,7 @@ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
|
||||
import { makeAuditMeta } from '../helpers/factories';
|
||||
|
||||
// vi.mock is hoisted to the top of the module — keep mocks there so vitest
|
||||
// vi.mock is hoisted to the top of the module - keep mocks there so vitest
|
||||
// doesn't warn about non-top-level calls. Use `vi.hoisted` for any mock that
|
||||
// references a value (mockQueueAdd) so it's evaluated before the mock factory
|
||||
// runs.
|
||||
@@ -48,7 +48,7 @@ beforeAll(async () => {
|
||||
await sql.end();
|
||||
dbAvailable = true;
|
||||
} catch {
|
||||
console.warn('[webhook-delivery] Test database not available — skipping integration tests');
|
||||
console.warn('[webhook-delivery] Test database not available - skipping integration tests');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user