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:
2026-05-23 00:52:59 +02:00
parent 43719b49e9
commit 221ae5784e
749 changed files with 7440 additions and 3118 deletions

View File

@@ -1,5 +1,5 @@
/**
* Task 8 aggregated projection (TDD).
* Task 8 - aggregated projection (TDD).
*
* Tests for:
* 1. listFilesAggregatedByEntity (4 cases)
@@ -8,7 +8,7 @@
*
* Fixture convention: makePort / makeClient / makeCompany / makeYacht from
* helpers/factories; TEST_USER_ID resolved once via beforeAll from a seeded
* user same pattern as document-folders-system-folders.test.ts.
* user - same pattern as document-folders-system-folders.test.ts.
*/
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
@@ -172,7 +172,7 @@ describe('files service · listFilesAggregatedByEntity', () => {
// File attached to the yacht at the time john owns it
await insertFile(portId, { yachtId, clientId: johnId });
// Transfer yacht to Mary (update currentOwner in place simulates transfer)
// Transfer yacht to Mary (update currentOwner in place - simulates transfer)
await db
.update(yachts)
.set({ currentOwnerType: 'client', currentOwnerId: maryId })
@@ -189,7 +189,7 @@ describe('files service · listFilesAggregatedByEntity', () => {
it("Mary's view does NOT see john's file (it has clientId=john, not mary)", async () => {
const result = await listFilesAggregatedByEntity(portId, 'client', maryId);
// Mary owns the yacht now, so FROM YACHT group will appear but the
// Mary owns the yacht now, so FROM YACHT group will appear - but the
// file has clientId=johnId (snapshotted FK), so it WON'T appear under
// Mary's DIRECTLY ATTACHED. The FROM YACHT group WILL appear since the
// file still has yachtId set.

View File

@@ -145,7 +145,7 @@ describe('maskSensitiveFields', () => {
});
it('does not over-mask innocuous "name" fields without PII context', () => {
// 'name' alone (port name, tag name, column name) must NOT be redacted
// 'name' alone (port name, tag name, column name) - must NOT be redacted
// unless it's part of first_name / last_name / full_name etc.
const result = maskSensitiveFields({
port_name: 'Port Nimara',

View File

@@ -1,5 +1,5 @@
/**
* EMAIL_REDIRECT_TO safety net comprehensive verification.
* EMAIL_REDIRECT_TO safety net - comprehensive verification.
*
* Goal: a single env flip (`EMAIL_REDIRECT_TO=<address>`) MUST pause every
* outbound communication channel. This test file exercises each channel
@@ -17,7 +17,7 @@ const REDIRECT_TARGET = 'redirect@example.test';
// 1. Documenso recipient redirect (createDocument + generateDocumentFromTemplate)
// -------------------------------------------------------------------------
describe('Documenso recipient redirect EMAIL_REDIRECT_TO', () => {
describe('Documenso recipient redirect - EMAIL_REDIRECT_TO', () => {
const originalRedirect = process.env.EMAIL_REDIRECT_TO;
const originalDocumensoUrl = process.env.DOCUMENSO_API_URL;
const originalDocumensoKey = process.env.DOCUMENSO_API_KEY;
@@ -52,7 +52,7 @@ describe('Documenso recipient redirect — EMAIL_REDIRECT_TO', () => {
vi.resetModules();
});
it('createDocument every recipient.email rewritten to redirect target', async () => {
it('createDocument - every recipient.email rewritten to redirect target', async () => {
vi.resetModules();
const mod = await import('@/lib/services/documenso-client');
await mod.createDocument('Test Doc', 'pdf-base64', [
@@ -70,7 +70,7 @@ describe('Documenso recipient redirect — EMAIL_REDIRECT_TO', () => {
}
});
it('generateDocumentFromTemplate formValues *Email keys rewritten', async () => {
it('generateDocumentFromTemplate - formValues *Email keys rewritten', async () => {
vi.resetModules();
const mod = await import('@/lib/services/documenso-client');
await mod.generateDocumentFromTemplate(42, {
@@ -89,7 +89,7 @@ describe('Documenso recipient redirect — EMAIL_REDIRECT_TO', () => {
expect(callBody.formValues['client.fullName']).toBe('Alice Smith');
});
it('generateDocumentFromTemplate recipients array rewritten (v2.x shape)', async () => {
it('generateDocumentFromTemplate - recipients array rewritten (v2.x shape)', async () => {
vi.resetModules();
const mod = await import('@/lib/services/documenso-client');
await mod.generateDocumentFromTemplate(42, {
@@ -106,7 +106,7 @@ describe('Documenso recipient redirect — EMAIL_REDIRECT_TO', () => {
}
});
it('sendDocument short-circuited when redirect is set (no /send call)', async () => {
it('sendDocument - short-circuited when redirect is set (no /send call)', async () => {
vi.resetModules();
const mod = await import('@/lib/services/documenso-client');
await mod.sendDocument('doc-1');
@@ -118,7 +118,7 @@ describe('Documenso recipient redirect — EMAIL_REDIRECT_TO', () => {
expect(sendCall).toBeUndefined();
});
it('sendReminder short-circuited when redirect is set (no /remind call)', async () => {
it('sendReminder - short-circuited when redirect is set (no /remind call)', async () => {
vi.resetModules();
const mod = await import('@/lib/services/documenso-client');
await mod.sendReminder('doc-1', 'signer-1');
@@ -126,7 +126,7 @@ describe('Documenso recipient redirect — EMAIL_REDIRECT_TO', () => {
expect(fetchMock).not.toHaveBeenCalled();
});
it('createDocument recipients NOT redirected when EMAIL_REDIRECT_TO unset', async () => {
it('createDocument - recipients NOT redirected when EMAIL_REDIRECT_TO unset', async () => {
delete process.env.EMAIL_REDIRECT_TO;
vi.resetModules();
const mod = await import('@/lib/services/documenso-client');
@@ -143,7 +143,7 @@ describe('Documenso recipient redirect — EMAIL_REDIRECT_TO', () => {
// 2. sendEmail redirect (covers the centralized path used by 5+ services)
// -------------------------------------------------------------------------
describe('sendEmail redirect EMAIL_REDIRECT_TO', () => {
describe('sendEmail redirect - EMAIL_REDIRECT_TO', () => {
const originalRedirect = process.env.EMAIL_REDIRECT_TO;
afterEach(() => {
@@ -174,7 +174,7 @@ describe('sendEmail redirect — EMAIL_REDIRECT_TO', () => {
}
// The mock is typed as `vi.fn(async () => …)` which gives `calls: unknown[]`
// so the indexer reads come back as possibly-undefined. The test arms
// - so the indexer reads come back as possibly-undefined. The test arms
// the spy and asserts toHaveBeenCalledOnce above, then this helper picks
// the first call with a runtime non-null check that satisfies tsc.
function firstSendMailArgs(spy: ReturnType<typeof vi.fn>): {
@@ -198,7 +198,7 @@ describe('sendEmail redirect — EMAIL_REDIRECT_TO', () => {
expect(args.subject).toMatch(/^\[redirected from alice@realclient\.com\] Welcome$/);
});
it('handles array of recipients joins original list into the subject prefix', async () => {
it('handles array of recipients - joins original list into the subject prefix', async () => {
const { sendMailMock, mod } = await setupWith(REDIRECT_TARGET);
await mod.sendEmail(['alice@realclient.com', 'bob@realclient.com'], 'Update', '<p>x</p>');
@@ -223,7 +223,7 @@ describe('sendEmail redirect — EMAIL_REDIRECT_TO', () => {
// 3. Webhook short-circuit (covers the per-port outbound webhook delivery)
// -------------------------------------------------------------------------
describe('Webhook short-circuit EMAIL_REDIRECT_TO', () => {
describe('Webhook short-circuit - EMAIL_REDIRECT_TO', () => {
// The actual webhook worker pulls from BullMQ + the DB. To keep this a
// pure unit test, we extract the "should I dispatch?" predicate and
// assert against env.EMAIL_REDIRECT_TO directly. The full integration

View File

@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
describe('Concurrent operation safety', () => {
it('concurrent interest score calculations should not interfere', async () => {
// Scoring is a pure read + compute operation no shared mutable state.
// Scoring is a pure read + compute operation - no shared mutable state.
// Simulates 10 parallel calculations to verify isolation.
const promises = Array.from({ length: 10 }, (_, i) =>
Promise.resolve({ interestId: `interest-${i}`, score: Math.random() * 100 }),
@@ -25,9 +25,7 @@ describe('Concurrent operation safety', () => {
payload: { clientId: `client-${i}` },
}));
const results = await Promise.allSettled(
events.map((e) => Promise.resolve(e)),
);
const results = await Promise.allSettled(events.map((e) => Promise.resolve(e)));
expect(results).toHaveLength(10);
expect(results.every((r) => r.status === 'fulfilled')).toBe(true);
@@ -39,9 +37,7 @@ describe('Concurrent operation safety', () => {
const readKpis = (portId: string) =>
Promise.resolve({ portId, totalClients: 120, activeInterests: 34 });
const results = await Promise.all(
Array.from({ length: 5 }, () => readKpis('port-abc')),
);
const results = await Promise.all(Array.from({ length: 5 }, () => readKpis('port-abc')));
results.forEach((r) => {
expect(r).toHaveProperty('portId', 'port-abc');
@@ -74,9 +70,7 @@ describe('Concurrent operation safety', () => {
const writeAuditEntry = (index: number) =>
Promise.resolve({ id: `audit-${Date.now()}-${index}`, index });
const entries = await Promise.all(
Array.from({ length: 20 }, (_, i) => writeAuditEntry(i)),
);
const entries = await Promise.all(Array.from({ length: 20 }, (_, i) => writeAuditEntry(i)));
const ids = entries.map((e) => e.id);
const uniqueIds = new Set(ids);
@@ -101,16 +95,14 @@ describe('Concurrent operation safety', () => {
const fulfilled = results.filter((r) => r.status === 'fulfilled');
const rejected = results.filter((r) => r.status === 'rejected');
// Indices 0, 3, 6, 9 fail 4 rejections, 6 successes.
// Indices 0, 3, 6, 9 fail - 4 rejections, 6 successes.
expect(fulfilled).toHaveLength(6);
expect(rejected).toHaveLength(4);
});
it('high-concurrency burst (50 simultaneous requests) all settle', async () => {
// Smoke-tests that the Promise machinery handles a realistic burst.
const burst = Array.from({ length: 50 }, (_, i) =>
Promise.resolve({ requestId: i }),
);
const burst = Array.from({ length: 50 }, (_, i) => Promise.resolve({ requestId: i }));
const results = await Promise.allSettled(burst);

View File

@@ -26,7 +26,7 @@ describe('PIPELINE_STAGES', () => {
]);
});
it('is a readonly tuple type-level immutability via `as const`', () => {
it('is a readonly tuple - type-level immutability via `as const`', () => {
const arr = PIPELINE_STAGES as unknown as string[];
expect(arr).toHaveLength(7);
});

View File

@@ -1,5 +1,5 @@
/**
* Tests for validateCustomFieldValue the private validation helper in
* Tests for validateCustomFieldValue - the private validation helper in
* custom-fields.service.ts. Since it is not exported we test it via the
* public setValues function, using vi.mock to avoid database calls.
* All assertions focus on what error message (if any) is thrown.
@@ -111,7 +111,7 @@ async function validate(
// ─── text ─────────────────────────────────────────────────────────────────────
describe('custom field validation text', () => {
describe('custom field validation - text', () => {
it('accepts a string value', async () => {
await expect(validate('text', 'hello')).resolves.toBeDefined();
});
@@ -131,7 +131,7 @@ describe('custom field validation — text', () => {
// ─── number ──────────────────────────────────────────────────────────────────
describe('custom field validation number', () => {
describe('custom field validation - number', () => {
it('accepts a valid number', async () => {
await expect(validate('number', 42)).resolves.toBeDefined();
});
@@ -151,7 +151,7 @@ describe('custom field validation — number', () => {
// ─── date ─────────────────────────────────────────────────────────────────────
describe('custom field validation date', () => {
describe('custom field validation - date', () => {
it('accepts a valid ISO date string', async () => {
await expect(validate('date', '2026-06-15')).resolves.toBeDefined();
});
@@ -171,7 +171,7 @@ describe('custom field validation — date', () => {
// ─── boolean ─────────────────────────────────────────────────────────────────
describe('custom field validation boolean', () => {
describe('custom field validation - boolean', () => {
it('accepts true', async () => {
await expect(validate('boolean', true)).resolves.toBeDefined();
});
@@ -191,7 +191,7 @@ describe('custom field validation — boolean', () => {
// ─── select ──────────────────────────────────────────────────────────────────
describe('custom field validation select', () => {
describe('custom field validation - select', () => {
const options = ['Small', 'Medium', 'Large'];
it('accepts a valid option', async () => {
@@ -220,7 +220,7 @@ describe('custom field validation — select', () => {
// ─── required / non-required null handling ───────────────────────────────────
describe('custom field validation required vs optional null', () => {
describe('custom field validation - required vs optional null', () => {
it('required field: null value → throws ValidationError', async () => {
await expect(validate('text', null, { isRequired: true })).rejects.toBeInstanceOf(
ValidationError,
@@ -234,7 +234,7 @@ describe('custom field validation — required vs optional null', () => {
});
it('non-required field: null value → succeeds (no error)', async () => {
// null for non-required means "clear the value" setValues will upsert null
// null for non-required means "clear the value" - setValues will upsert null
await expect(validate('text', null, { isRequired: false })).resolves.toBeDefined();
});
});

View File

@@ -1,10 +1,10 @@
/**
* Match-finding library unit tests.
* Match-finding library - unit tests.
*
* Each duplicate cluster from the legacy NocoDB Interests audit (see
* docs/superpowers/specs/2026-05-03-dedup-and-migration-design.md §1.2)
* is encoded as a fixture here. The expected scoring tier (high / medium
* / low) is the design contract if the algorithm starts returning
* / low) is the design contract - if the algorithm starts returning
* "high" for a Pattern F case (Etiennette / Bruno+Bruce) it has lost
* the false-positive guard and we'll know immediately.
*/
@@ -12,7 +12,7 @@ import { describe, expect, it } from 'vitest';
import { findClientMatches, type MatchCandidate } from '@/lib/dedup/find-matches';
// Sensible defaults for tests match the design's recommended thresholds.
// Sensible defaults for tests - match the design's recommended thresholds.
const THRESHOLDS = {
highScore: 90,
mediumScore: 50,
@@ -30,7 +30,7 @@ function candidate(partial: Partial<MatchCandidate> & { id: string }): MatchCand
}
describe('findClientMatches', () => {
describe('Pattern A pure double-submit (high confidence)', () => {
describe('Pattern A - pure double-submit (high confidence)', () => {
it('flags identical email + phone as high', () => {
// From real data: Deepak Ramchandani #624/#625, identical fields.
const incoming = candidate({
@@ -60,7 +60,7 @@ describe('findClientMatches', () => {
});
});
describe('Pattern B same email, different phone format (high)', () => {
describe('Pattern B - same email, different phone format (high)', () => {
it('high confidence when phones already normalize-equal', () => {
// From real data: Howard Wiarda #236/#536, "574-274-0548" vs "+15742740548".
// After normalization both phones are the same E.164, so the rule fires.
@@ -88,7 +88,7 @@ describe('findClientMatches', () => {
});
});
describe('Pattern C name capitalization variant (high)', () => {
describe('Pattern C - name capitalization variant (high)', () => {
it('treats lowercase + uppercase as the same person when surname-token + email + phone all match', () => {
// From real data: Nicolas Ruiz #681/#682/#683, email differs only by case.
const incoming = candidate({
@@ -114,7 +114,7 @@ describe('findClientMatches', () => {
});
});
describe('Pattern D name shortening (high)', () => {
describe('Pattern D - name shortening (high)', () => {
it('Chris vs Christopher with same email + phone scores high', () => {
// From real data: Chris Allen #700 vs Christopher Allen #534.
const incoming = candidate({
@@ -140,9 +140,9 @@ describe('findClientMatches', () => {
});
});
describe('Pattern E typo on resubmit', () => {
describe('Pattern E - typo on resubmit', () => {
it('same email + nearly-identical phone (typo in last digits) scores high', () => {
// Christopher Camazou #649/#650 phone differs in last 4 digits but
// Christopher Camazou #649/#650 - phone differs in last 4 digits but
// everything else matches. Exact phone equality fails; email exact
// match alone (60) + name-token match (20) puts us in medium tier.
// The user can confirm the merge.
@@ -166,15 +166,15 @@ describe('findClientMatches', () => {
const matches = findClientMatches(incoming, pool, THRESHOLDS);
expect(matches).toHaveLength(1);
// Email + name match without phone match strong but not certain.
// Email + name match without phone match - strong but not certain.
expect(matches[0]!.confidence).toMatch(/^(high|medium)$/);
expect(matches[0]!.score).toBeGreaterThanOrEqual(70);
});
it('Constanzo / Costanzo surname typo with same email + phone scores high', () => {
// Gianfranco Di Constanzo #585 vs Di Costanzo #336 same email + phone
// Gianfranco Di Constanzo #585 vs Di Costanzo #336 - same email + phone
// and only a 1-letter surname typo. This is a strong "same client,
// multiple yachts" signal the design's signature win.
// multiple yachts" signal - the design's signature win.
const incoming = candidate({
id: 'b',
fullName: 'Gianfranco Di Constanzo',
@@ -199,9 +199,9 @@ describe('findClientMatches', () => {
});
});
describe('Pattern F hard cases (must NOT auto-merge)', () => {
describe('Pattern F - hard cases (must NOT auto-merge)', () => {
it('same name with different country phone + different email scores at most medium', () => {
// Etiennette Clamouze #188/#717 same name but completely different
// Etiennette Clamouze #188/#717 - same name but completely different
// email + phone (and the phones are in different country codes,
// suggesting either a relative, a coworker, or a name-collision).
// We must NOT classify this as "high" or it would force-merge two
@@ -236,7 +236,7 @@ describe('findClientMatches', () => {
});
it('shared email between two clearly different names is medium not high', () => {
// Bruno Joyerot #18 vs Bruce Hearn #19 Bruno's row shows email
// Bruno Joyerot #18 vs Bruce Hearn #19 - Bruno's row shows email
// belonging to "catherine elaine hearn" (Bruce's spouse). Same
// household phone area code. Name overlap is partial. Don't merge.
const incoming = candidate({
@@ -258,7 +258,7 @@ describe('findClientMatches', () => {
const matches = findClientMatches(incoming, pool, THRESHOLDS);
// Names don't match, emails don't match, phones differ there's
// Names don't match, emails don't match, phones differ - there's
// no reason for this to surface at all. Either no match or low.
if (matches.length > 0) {
expect(matches[0]!.confidence).toBe('low');
@@ -266,7 +266,7 @@ describe('findClientMatches', () => {
});
});
describe('Negative evidence same email but different country phone', () => {
describe('Negative evidence - same email but different country phone', () => {
it('reduces score when email matches but phone country differs', () => {
// Constructed: same email, but one phone is +33 (FR) and the other
// is +1 (US). Likely a shared-inbox spouse situation. We want
@@ -298,7 +298,7 @@ describe('findClientMatches', () => {
});
});
describe('Blocking only relevant candidates are scored', () => {
describe('Blocking - only relevant candidates are scored', () => {
it('does not score candidates with no shared emails / phones / surname token', () => {
const incoming = candidate({
id: 'newbie',
@@ -352,7 +352,7 @@ describe('findClientMatches', () => {
});
const pool = [
candidate({
// High match same email + phone
// High match - same email + phone
id: 'high-match',
fullName: 'John Smith',
surnameToken: 'smith',
@@ -360,7 +360,7 @@ describe('findClientMatches', () => {
phonesE164: ['+15551234567'],
}),
candidate({
// Medium match same email only
// Medium match - same email only
id: 'medium-match',
fullName: 'Different Person',
surnameToken: 'person',

View File

@@ -1,5 +1,5 @@
/**
* Migration transform fixture-based regression test.
* Migration transform - fixture-based regression test.
*
* Feeds the transform a small frozen NocoDB snapshot containing one
* representative row from each duplicate pattern documented in
@@ -59,7 +59,7 @@ const FIXTURE: NocoDbSnapshot = {
'Sales Process Level': 'General Qualified Interest',
}),
// Pattern C: name capitalization (Nicolas Ruiz #681/#682/#683 three rows)
// Pattern C: name capitalization (Nicolas Ruiz #681/#682/#683 - three rows)
row({
Id: 681,
'Full Name': 'Nicolas Ruiz',
@@ -127,7 +127,7 @@ const FIXTURE: NocoDbSnapshot = {
],
};
describe('transformSnapshot fixture regression', () => {
describe('transformSnapshot - fixture regression', () => {
it('produces the expected number of clients + interests', () => {
const plan = transformSnapshot(FIXTURE);
@@ -203,7 +203,7 @@ describe('transformSnapshot — fixture regression', () => {
});
it('produces deterministic output (same input → same plan)', () => {
// The transform is pure running it twice should yield bit-identical
// The transform is pure - running it twice should yield bit-identical
// results. Catches order-dependent bugs in the dedup clustering.
const a = transformSnapshot(FIXTURE);
const b = transformSnapshot(FIXTURE);
@@ -214,7 +214,7 @@ describe('transformSnapshot — fixture regression', () => {
// ─── EOI document derivation ───────────────────────────────────────────────
describe('transformSnapshot EOI document derivation', () => {
describe('transformSnapshot - EOI document derivation', () => {
/**
* A fixture row that mimics a fully-signed legacy interest with a
* Documenso ID, all three signing slots populated, and an S3 path.
@@ -301,7 +301,7 @@ describe('transformSnapshot — EOI document derivation', () => {
eoiFixture({
Id: 800,
documensoID: '200',
// No EOI Status, no developer sign only client has signed.
// No EOI Status, no developer sign - only client has signed.
clientSignTime: '2026-04-01T12:00:00.000Z',
}),
);
@@ -395,7 +395,7 @@ describe('parseFlexibleDate format handling', () => {
});
});
describe('transformSnapshot residential leads', () => {
describe('transformSnapshot - residential leads', () => {
it('produces one PlannedResidentialClient per source row', () => {
const plan = transformSnapshot({
fetchedAt: '2026-05-04T00:00:00.000Z',

View File

@@ -1,5 +1,5 @@
/**
* Normalization library unit tests.
* Normalization library - unit tests.
*
* Every fixture here comes from real dirty values observed in the legacy
* NocoDB Interests table during the 2026-05-03 audit (see
@@ -90,7 +90,7 @@ describe('normalizeName', () => {
expect(normalizeName("Liam O'Brien").surnameToken).toBe("o'brien");
});
it('handles single-token names surnameToken is the only token', () => {
it('handles single-token names - surnameToken is the only token', () => {
expect(normalizeName('Madonna').surnameToken).toBe('madonna');
});
@@ -121,7 +121,7 @@ describe('normalizeEmail', () => {
expect(normalizeEmail('Hef355@yahoo.com')).toBe('hef355@yahoo.com');
});
it('preserves plus-aliases both legitimate and tricks', () => {
it('preserves plus-aliases - both legitimate and tricks', () => {
// Per design §3.2: "+aliases" are not stripped. Compare by full localpart.
expect(normalizeEmail('marcus+sales@example.com')).toBe('marcus+sales@example.com');
});
@@ -180,7 +180,7 @@ describe('normalizePhone', () => {
});
it('flags placeholder all-zeros numbers and returns null', () => {
// From real data: "+447000000000" (#641, "Milos Vitkovic" clearly fake).
// From real data: "+447000000000" (#641, "Milos Vitkovic" - clearly fake).
const out = normalizePhone('+447000000000', 'GB');
expect(out?.flagged).toBe('placeholder');
expect(out?.e164).toBeNull();

View File

@@ -6,7 +6,7 @@
* - `applyEntityRestoredSuffix` no-op when the folder was never archived
* (must not flip archived_at, must not rename anything, must not emit
* an audit log).
* - `syncEntityFolderName` collision loop past `(2)` proves the suffix
* - `syncEntityFolderName` collision loop past `(2)` - proves the suffix
* loop iterates correctly when the first numbered candidate is also
* taken. Existing coverage only asserted the `(2)` case.
*
@@ -14,7 +14,7 @@
* `partially_signed → 'partial'` mapping, but that helper currently lives
* inside React component files (`entity-folder-view.tsx`,
* `signing-details-dialog.tsx`, `documents-hub.tsx`) and is not exported.
* A real unit test would require extracting it to a shared util out of
* A real unit test would require extracting it to a shared util - out of
* scope for this subagent's file ownership. See the audit report for the
* deferred fix.
*/
@@ -73,13 +73,13 @@ describe('document-folders · applyEntityRestoredSuffix no-op (regression)', ()
});
expect(after?.name).toBe(originalName);
expect(after?.archivedAt).toBeNull();
// updatedAt should not advance on a no-op restore the row write is
// updatedAt should not advance on a no-op restore - the row write is
// skipped entirely.
expect(after?.updatedAt?.getTime()).toBe(before?.updatedAt?.getTime());
});
it('is a no-op when called for an entity whose folder does not exist (lazy creation)', async () => {
// Different port no folder for this client.
// Different port - no folder for this client.
const otherPort = await makePort();
await ensureSystemRoots(otherPort.id, TEST_USER_ID);
const [other] = await db
@@ -117,7 +117,7 @@ describe('document-folders · syncEntityFolderName collision loop > (2) (regress
});
it('walks past (2) → (3) when the (2) suffix is also taken', async () => {
// Three clients with the same name first two are pre-created with their
// Three clients with the same name - first two are pre-created with their
// entity folders so `sharedName` and `sharedName (2)` are both occupied
// before we trigger the rename on the third.
const sharedName = `Triple Collision ${crypto.randomUUID().slice(0, 6)}`;
@@ -127,10 +127,10 @@ describe('document-folders · syncEntityFolderName collision loop > (2) (regress
const [second] = await db.insert(clients).values({ portId, fullName: sharedName }).returning();
const secondFolder = await ensureEntityFolder(portId, 'client', second!.id, TEST_USER_ID);
// Sanity second client's folder is the "(2)" variant.
// Sanity - second client's folder is the "(2)" variant.
expect(secondFolder.name).toBe(`${sharedName} (2)`);
// Third client start with a different name so its folder is unique,
// Third client - start with a different name so its folder is unique,
// then rename it to the shared name to force `syncEntityFolderName` to
// walk past (2).
const placeholderName = `Triple Collision Placeholder ${crypto.randomUUID().slice(0, 6)}`;

View File

@@ -1,9 +1,9 @@
/**
* Task 2 ensureSystemRoots (TDD).
* Task 3 ensureEntityFolder (TDD).
* Task 2 - ensureSystemRoots (TDD).
* Task 3 - ensureEntityFolder (TDD).
*
* Fixture convention: makePort from helpers/factories (async DB insert);
* TEST_USER_ID resolved once via beforeAll from a seeded user same pattern
* TEST_USER_ID resolved once via beforeAll from a seeded user - same pattern
* as document-folders-crud.test.ts and alerts-tenant-isolation.test.ts.
*/
@@ -48,7 +48,7 @@ describe('document-folders service · ensureSystemRoots', () => {
}
});
it('is idempotent second call does not create duplicates', async () => {
it('is idempotent - second call does not create duplicates', async () => {
await ensureSystemRoots(portId, TEST_USER_ID);
await ensureSystemRoots(portId, TEST_USER_ID);
const rows = await db
@@ -97,7 +97,7 @@ describe('document-folders service · ensureEntityFolder', () => {
expect(folder.name).toBe(row!.fullName);
});
it('is idempotent returns the same row on second call', async () => {
it('is idempotent - returns the same row on second call', async () => {
const a = await ensureEntityFolder(portId, 'client', clientId, TEST_USER_ID);
const b = await ensureEntityFolder(portId, 'client', clientId, TEST_USER_ID);
expect(a.id).toBe(b.id);
@@ -282,7 +282,7 @@ describe('document-folders service · archive lifecycle', () => {
expect(folder?.systemManaged).toBe(true);
});
it('is idempotent on archive second call does not double-append', async () => {
it('is idempotent on archive - second call does not double-append', async () => {
await applyEntityArchivedSuffix(portId, 'client', clientId);
await applyEntityArchivedSuffix(portId, 'client', clientId);
const folder = await db.query.documentFolders.findFirst({

View File

@@ -42,7 +42,7 @@ describe('document-folder validators', () => {
// ─── folderId='' → null transform (regression) ─────────────────────────────
//
// The frontend's URL-query builder emits `?folderId=` (empty string) when
// the user picks "All documents" without the transform, Zod would parse
// the user picks "All documents" - without the transform, Zod would parse
// this as the literal string "" and the SQL layer would try to JOIN on an
// empty folder id, returning zero rows instead of the expected unscoped
// result. The transform lives on `listDocumentsSchema` (and

View File

@@ -43,8 +43,8 @@ describe('parseImportPath', () => {
});
it('preserves special characters in folder names', () => {
expect(parseImportPath('', "Q1 Year's End/contract & rider.pdf")).toEqual({
folderSegments: ["Q1 Year's End"],
expect(parseImportPath('', "Q1 - Year's End/contract & rider.pdf")).toEqual({
folderSegments: ["Q1 - Year's End"],
filename: 'contract & rider.pdf',
});
});

View File

@@ -1,5 +1,5 @@
/**
* Phase 7 validator-level guarantees for the send-out flow.
* Phase 7 - validator-level guarantees for the send-out flow.
*
* §14.7 mitigation: recipient typo (the strict email regex is the first
* line of defense; the confirmation modal is the second).

View File

@@ -6,10 +6,10 @@ describe('formatDate', () => {
const REF = '2026-05-12T14:30:45.000Z';
it('returns fallback for null/undefined/invalid', () => {
expect(formatDate(null)).toBe('');
expect(formatDate(undefined)).toBe('');
expect(formatDate('not a date')).toBe('');
expect(formatDate(NaN)).toBe('');
expect(formatDate(null)).toBe('-');
expect(formatDate(undefined)).toBe('-');
expect(formatDate('not a date')).toBe('-');
expect(formatDate(NaN)).toBe('-');
expect(formatDate(null, 'date.medium', { fallback: 'N/A' })).toBe('N/A');
});
@@ -35,7 +35,7 @@ describe('formatDate', () => {
expect(out.toLowerCase()).toContain('may');
});
it('respects timezone datetime.short in different zones', () => {
it('respects timezone - datetime.short in different zones', () => {
const ny = formatDate(REF, 'datetime.short', { tz: 'America/New_York' });
const utc = formatDate(REF, 'datetime.short', { tz: 'UTC' });
expect(ny).not.toBe(utc);
@@ -55,7 +55,7 @@ describe('formatDate', () => {
describe('formatDateRange', () => {
it('handles missing start/end', () => {
expect(formatDateRange(null, null)).toBe('');
expect(formatDateRange(null, null)).toBe('-');
expect(formatDateRange('2026-05-12', null)).toMatch(/→$/);
expect(formatDateRange(null, '2026-05-12')).toMatch(/^→/);
});
@@ -107,6 +107,6 @@ describe('formatRelative', () => {
});
it('returns fallback for invalid input', () => {
expect(formatRelative(null)).toBe('');
expect(formatRelative(null)).toBe('-');
});
});

View File

@@ -10,7 +10,7 @@ import {
/**
* Pure-logic tests for the realtime-invalidation subscription helper. The
* React hook (`useRealtimeInvalidation`) is just a thin wrapper around this
* function verifying the handler-registration / fire-time-lookup behavior
* function - verifying the handler-registration / fire-time-lookup behavior
* here is sufficient to lock in the bug fixes:
* 1. Re-subscribe storm (caller passing inline literals)
* 2. Fresh queryKeys read at fire-time
@@ -129,7 +129,7 @@ describe('subscribeRealtimeInvalidations', () => {
cleanup();
expect(offCalls.map((c) => c.event).sort()).toEqual(['a:event', 'b:event']);
// All listeners removed emitting after cleanup invalidates nothing.
// All listeners removed - emitting after cleanup invalidates nothing.
emit('a:event');
emit('b:event');
expect(calls).toEqual([]);
@@ -150,7 +150,7 @@ describe('subscribeRealtimeInvalidations', () => {
};
subscribeRealtimeInvalidations(socket, ['client:updated'], queryClient, () => currentMap);
// Wipe the entry handler will fire but find nothing to invalidate.
// Wipe the entry - handler will fire but find nothing to invalidate.
currentMap = {};
expect(() => emit('client:updated')).not.toThrow();
expect(calls).toEqual([]);

View File

@@ -1,5 +1,5 @@
/**
* i18n PR1 country dataset.
* i18n PR1 - country dataset.
*
* Validates:
* 1. The dataset includes every common ISO-3166-1 alpha-2 code we'd

View File

@@ -1,5 +1,5 @@
/**
* i18n PR2 phone helpers.
* i18n PR2 - phone helpers.
*
* Validates:
* 1. parsePhone yields E.164 + country + display formats

View File

@@ -1,5 +1,5 @@
/**
* i18n PR4 subdivisions.
* i18n PR4 - subdivisions.
*
* Validates:
* 1. Major countries (PL, US, CA, GB, AU) return non-empty lists

View File

@@ -1,5 +1,5 @@
/**
* i18n PR3 timezone helpers.
* i18n PR3 - timezone helpers.
*
* Validates:
* 1. Single-zone countries return one IANA string

View File

@@ -18,7 +18,7 @@ async function makePng(
.toBuffer();
}
describe('processLogoUpload sharp pipeline', () => {
describe('processLogoUpload - sharp pipeline', () => {
it('accepts a healthy PNG with alpha and resizes if needed', async () => {
const buf = await makePng(2400, 800);
const result = await processLogoUpload(buf);
@@ -40,7 +40,7 @@ describe('processLogoUpload — sharp pipeline', () => {
});
it('rejects buffers that exceed the raw size cap', async () => {
// 6 MB of zero bytes fails the raw size cap before sharp parses.
// 6 MB of zero bytes - fails the raw size cap before sharp parses.
const buf = Buffer.alloc(6 * 1024 * 1024);
await expect(processLogoUpload(buf)).rejects.toThrow(/MB/i);
});

View File

@@ -3,7 +3,7 @@
*
* Every code path that turns rep-authored markdown into the email's
* `html` body is required to go through `renderEmailBody()`. These tests
* are the canary if any future change to the renderer lets a known XSS
* are the canary - if any future change to the renderer lets a known XSS
* payload through, the test breaks before the change ships.
*/
import { describe, expect, it } from 'vitest';
@@ -16,7 +16,7 @@ import {
renderEmailBody,
} from '@/lib/utils/markdown-email';
describe('renderEmailBody XSS payload coverage', () => {
describe('renderEmailBody - XSS payload coverage', () => {
it('escapes <script> tags so they render as text, not active script', () => {
const html = renderEmailBody('Hi <script>alert(1)</script> there');
expect(html).not.toContain('<script>');
@@ -68,7 +68,7 @@ describe('renderEmailBody — XSS payload coverage', () => {
it('escapes attribute-style XSS attempts (no live <svg> tag survives)', () => {
const html = renderEmailBody('"><svg onload=alert(1)>');
// The literal "<svg" must never appear unescaped the angle bracket is
// The literal "<svg" must never appear unescaped - the angle bracket is
// what the browser parses, not the word "onload".
expect(html).not.toContain('<svg');
expect(html).toContain('&lt;svg');
@@ -92,7 +92,7 @@ describe('renderEmailBody — XSS payload coverage', () => {
});
});
describe('renderEmailBody markdown rules', () => {
describe('renderEmailBody - markdown rules', () => {
it('renders **bold** as <strong>', () => {
expect(renderEmailBody('this is **bold**')).toContain('<strong>bold</strong>');
});

View File

@@ -71,7 +71,7 @@ describe('PDF report renderer', () => {
const buf = await renderToBuffer(element as any);
expect(buf.byteLength).toBeGreaterThan(2_000);
// PDF files start with `%PDF-` magic bytes sanity-check that
// PDF files start with `%PDF-` magic bytes - sanity-check that
// the renderer produced an actual PDF, not an error blob or
// empty buffer.
const head = buf.subarray(0, 5).toString('utf-8');
@@ -96,7 +96,7 @@ describe('PDF report renderer', () => {
occupancyRate: 0,
},
// Provide pipelineCounts even though widgetIds didn't ask for
// it the renderer should still skip the section since it's
// it - the renderer should still skip the section since it's
// gated on widgetIds, not data presence.
pipelineCounts: [{ stage: 'enquiry', count: 1 }],
},

View File

@@ -97,7 +97,7 @@ describe('fillEoiFormFields', () => {
const filled = await fillEoiFormFields(sourcePdf, makeContext());
const out = await PDFDocument.load(filled);
// Flatten removes the interactive fields from the form the values
// Flatten removes the interactive fields from the form - the values
// are baked into the page content stream so the signer can't edit
// them after the fact.
expect(out.getForm().getFields()).toEqual([]);
@@ -114,7 +114,7 @@ describe('fillEoiFormFields', () => {
const filled = await fillEoiFormFields(sourcePdf, makeContext());
// Round-trip the saved bytes. With the AcroForm flattened, the doc
// still loads as a valid PDF and retains the original page count
// still loads as a valid PDF and retains the original page count -
// the text-field widgets have been baked into the content stream.
const out = await PDFDocument.load(filled);
expect(out.getPageCount()).toBe(1);
@@ -138,7 +138,7 @@ describe('fillEoiFormFields', () => {
);
// Still flattens cleanly and round-trips as a valid PDF even with
// null email/address the doc doesn't error out.
// null email/address - the doc doesn't error out.
const out = await PDFDocument.load(filled);
expect(out.getForm().getFields()).toEqual([]);
expect(out.getTitle()).toBe('EOI Bob');

View File

@@ -24,7 +24,7 @@ beforeAll(() => {
// ─────────────────────────────────────────────────────────────────────────────
describe('AES-256-GCM plaintext non-exposure', () => {
describe('AES-256-GCM - plaintext non-exposure', () => {
it('encrypted output does not contain the plaintext', async () => {
const { encrypt } = await import('@/lib/utils/encryption');
const plaintext = 'my-secret-password';
@@ -65,7 +65,7 @@ describe('AES-256-GCM — plaintext non-exposure', () => {
});
});
describe('AES-256-GCM IV randomness (semantic security)', () => {
describe('AES-256-GCM - IV randomness (semantic security)', () => {
it('different plaintexts produce different ciphertexts', async () => {
const { encrypt } = await import('@/lib/utils/encryption');
const enc1 = encrypt('password1');
@@ -77,7 +77,7 @@ describe('AES-256-GCM — IV randomness (semantic security)', () => {
const { encrypt } = await import('@/lib/utils/encryption');
const enc1 = encrypt('same-password');
const enc2 = encrypt('same-password');
// IVs differ, so ciphertexts differ prevents ciphertext comparison attacks
// IVs differ, so ciphertexts differ - prevents ciphertext comparison attacks
expect(enc1).not.toBe(enc2);
});
@@ -93,7 +93,7 @@ describe('AES-256-GCM — IV randomness (semantic security)', () => {
});
});
describe('AES-256-GCM authenticated encryption (tamper detection)', () => {
describe('AES-256-GCM - authenticated encryption (tamper detection)', () => {
it('tampered data field throws on decrypt', async () => {
const { encrypt, decrypt } = await import('@/lib/utils/encryption');
const encrypted = encrypt('test');
@@ -134,7 +134,7 @@ describe('AES-256-GCM — authenticated encryption (tamper detection)', () => {
});
});
describe('AES-256-GCM decryption correctness', () => {
describe('AES-256-GCM - decryption correctness', () => {
it('decrypt recovers original plaintext', async () => {
const { encrypt, decrypt } = await import('@/lib/utils/encryption');
const plaintext = 'my-secret-credentials';
@@ -161,7 +161,7 @@ describe('AES-256-GCM — decryption correctness', () => {
});
});
describe('AES-256-GCM key validation', () => {
describe('AES-256-GCM - key validation', () => {
it('throws when EMAIL_CREDENTIAL_KEY is not set', async () => {
const { encrypt } = await import('@/lib/utils/encryption');
const saved = process.env.EMAIL_CREDENTIAL_KEY;

View File

@@ -55,7 +55,7 @@ import {
// ─────────────────────────────────────────────────────────────────────────────
describe('Error response security AppError subclasses', () => {
describe('Error response security - AppError subclasses', () => {
it('AppError returns correct status without leaking constructor args', async () => {
const error = new AppError(400, 'Bad request', 'BAD_REQUEST');
const response = errorResponse(error);
@@ -110,7 +110,7 @@ describe('Error response security — AppError subclasses', () => {
});
});
describe('Error response security unknown / native errors', () => {
describe('Error response security - unknown / native errors', () => {
it('native Error with SQL content returns generic 500', async () => {
const error = new Error('SELECT * FROM users WHERE id = 1; DROP TABLE users;--');
const response = errorResponse(error);
@@ -170,7 +170,7 @@ describe('Error response security — unknown / native errors', () => {
});
});
describe('Error response security ZodError', () => {
describe('Error response security - ZodError', () => {
it('ZodError returns 400 with VALIDATION_ERROR code', async () => {
const { ZodError, ZodIssueCode } = await import('zod');
const error = new ZodError([
@@ -214,7 +214,7 @@ describe('Error response security — ZodError', () => {
});
});
describe('Error response security response shape invariants', () => {
describe('Error response security - response shape invariants', () => {
it('every AppError response body follows { error, code } shape', async () => {
const errors = [
new AppError(400, 'Bad request', 'BAD_REQUEST'),

View File

@@ -21,7 +21,7 @@ describe('SQL injection prevention via Zod schemas', () => {
fullName: "Robert'); DROP TABLE clients;--",
contacts: [{ channel: 'email', value: 'test@example.com' }],
});
// Zod must accept this as a valid string we rely on Drizzle for SQL safety
// Zod must accept this as a valid string - we rely on Drizzle for SQL safety
expect(result.success).toBe(true);
if (result.success) {
// The payload passes through unchanged; the query layer uses $1 placeholders
@@ -61,7 +61,7 @@ describe('SQL injection prevention via Zod schemas', () => {
const result = searchQuerySchema.safeParse({
q: "'; DROP TABLE clients;--",
});
// Min length 2, so this passes Drizzle uses $1 for the actual query
// Min length 2, so this passes - Drizzle uses $1 for the actual query
expect(result.success).toBe(true);
});
@@ -95,7 +95,7 @@ describe('SQL injection prevention via Zod schemas', () => {
// ─────────────────────────────────────────────────────────────────────────────
describe('File upload validation MIME type allowlist', () => {
describe('File upload validation - MIME type allowlist', () => {
it('rejects application/x-executable (binary/shellcode)', async () => {
const { ALLOWED_MIME_TYPES } = await import('@/lib/constants/file-validation');
expect(ALLOWED_MIME_TYPES.has('application/x-executable')).toBe(false);
@@ -156,9 +156,7 @@ describe('File upload validation — MIME type allowlist', () => {
).toBe(true);
expect(ALLOWED_MIME_TYPES.has('application/vnd.ms-excel')).toBe(true);
expect(
ALLOWED_MIME_TYPES.has(
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
),
ALLOWED_MIME_TYPES.has('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'),
).toBe(true);
});
@@ -169,7 +167,7 @@ describe('File upload validation — MIME type allowlist', () => {
});
});
describe('File upload validation size limit', () => {
describe('File upload validation - size limit', () => {
it('MAX_FILE_SIZE is exactly 50 MB (52_428_800 bytes)', async () => {
const { MAX_FILE_SIZE } = await import('@/lib/constants/file-validation');
expect(MAX_FILE_SIZE).toBe(50 * 1024 * 1024);

View File

@@ -18,7 +18,7 @@ import { deepMerge } from '@/lib/api/helpers';
// ─────────────────────────────────────────────────────────────────────────────
describe('deepMerge basic override behaviour', () => {
describe('deepMerge - basic override behaviour', () => {
it('override replaces a single base value', () => {
const base = { clients: { view: true, create: true, delete: false } };
const override = { clients: { delete: true } };
@@ -52,7 +52,7 @@ describe('deepMerge — basic override behaviour', () => {
});
});
describe('deepMerge nested structure preservation', () => {
describe('deepMerge - nested structure preservation', () => {
it('deep merges two levels of nesting without data loss', () => {
const base = { admin: { manage_users: false, manage_settings: true } };
const override = { admin: { manage_users: true } };
@@ -65,7 +65,10 @@ describe('deepMerge — nested structure preservation', () => {
const base = { reports: { export: { csv: true, pdf: false } } };
const override = { reports: { export: { pdf: true } } };
const result = deepMerge(base, override);
const exportPerms = (result.reports as Record<string, unknown>).export as Record<string, boolean>;
const exportPerms = (result.reports as Record<string, unknown>).export as Record<
string,
boolean
>;
expect(exportPerms.pdf).toBe(true);
expect(exportPerms.csv).toBe(true);
});
@@ -89,7 +92,7 @@ describe('deepMerge — nested structure preservation', () => {
});
});
describe('deepMerge immutability', () => {
describe('deepMerge - immutability', () => {
it('does not mutate the target object', () => {
const base = { clients: { view: true, delete: false } };
const override = { clients: { delete: true } };
@@ -106,7 +109,7 @@ describe('deepMerge — immutability', () => {
});
});
describe('deepMerge edge cases', () => {
describe('deepMerge - edge cases', () => {
it('empty override returns a copy of the base', () => {
const base = { clients: { view: true } };
const result = deepMerge(base, {});
@@ -126,7 +129,7 @@ describe('deepMerge — edge cases', () => {
it('scalar override value wins over nested base value (array not merged)', () => {
// When source has a non-object value for a key that base has as an object,
// the source scalar replaces the base object this is the defined behaviour
// the source scalar replaces the base object - this is the defined behaviour
const base = { meta: { x: 1 } };
const override = { meta: 'string-value' };
const result = deepMerge(base, override as unknown as Record<string, unknown>);

View File

@@ -16,7 +16,7 @@ import { maskSensitiveFields } from '@/lib/audit';
// ─────────────────────────────────────────────────────────────────────────────
describe('Sensitive data masking field detection', () => {
describe('Sensitive data masking - field detection', () => {
it('masks "email" field', () => {
const result = maskSensitiveFields({ email: 'user@example.com' });
expect(result?.email).not.toBe('user@example.com');
@@ -48,7 +48,7 @@ describe('Sensitive data masking — field detection', () => {
});
});
describe('Sensitive data masking masking format', () => {
describe('Sensitive data masking - masking format', () => {
it('long email (len > 4) uses partial mask: first 2 + *** + last 2', () => {
// 'user@example.com' → 'us***om'
const result = maskSensitiveFields({ email: 'user@example.com' });
@@ -84,7 +84,7 @@ describe('Sensitive data masking — masking format', () => {
});
});
describe('Sensitive data masking non-sensitive fields', () => {
describe('Sensitive data masking - non-sensitive fields', () => {
it('preserves string non-sensitive fields unchanged', () => {
const result = maskSensitiveFields({ name: 'John Smith', status: 'active' });
expect(result?.name).toBe('John Smith');
@@ -122,7 +122,7 @@ describe('Sensitive data masking — non-sensitive fields', () => {
});
});
describe('Sensitive data masking edge cases', () => {
describe('Sensitive data masking - edge cases', () => {
it('returns undefined for undefined input', () => {
expect(maskSensitiveFields(undefined)).toBeUndefined();
});
@@ -139,7 +139,7 @@ describe('Sensitive data masking — edge cases', () => {
expect(original.email).toBe(originalEmail);
});
it('only masks string values non-string sensitive fields are left as-is', () => {
it('only masks string values - non-string sensitive fields are left as-is', () => {
// e.g. if someone stores a number in an "email" field (type error upstream),
// the masking logic gracefully skips it (typeof check)
const result = maskSensitiveFields({ email: 12345 as unknown as string });

View File

@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`W7 snapshots heat at canonical inputs > heat: cold (no history) 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: cold (no history) 1`] = `
{
"eoiCount": 0,
"furthestStage": 0,
@@ -10,7 +10,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: cold (no history) 1`]
}
`;
exports[`W7 snapshots heat at canonical inputs > heat: no fallthrough but many interests 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: no fallthrough but many interests 1`] = `
{
"eoiCount": 15,
"furthestStage": 0,
@@ -20,7 +20,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: no fallthrough but ma
}
`;
exports[`W7 snapshots heat at canonical inputs > heat: old fallthrough at deposit stage (recency decayed) 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: old fallthrough at deposit stage (recency decayed) 1`] = `
{
"eoiCount": 15,
"furthestStage": 40,
@@ -30,7 +30,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: old fallthrough at de
}
`;
exports[`W7 snapshots heat at canonical inputs > heat: recent fallthrough at deposit stage (deepest hurt) 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: recent fallthrough at deposit stage (deepest hurt) 1`] = `
{
"eoiCount": 15,
"furthestStage": 40,
@@ -40,7 +40,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: recent fallthrough at
}
`;
exports[`W7 snapshots heat at canonical inputs > heat: recent fallthrough at enquiry stage 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: recent fallthrough at enquiry stage 1`] = `
{
"eoiCount": 0,
"furthestStage": 0,
@@ -50,7 +50,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: recent fallthrough at
}
`;
exports[`W7 snapshots heat at canonical inputs > heat: recent fallthrough at eoi stage 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: recent fallthrough at eoi stage 1`] = `
{
"eoiCount": 5,
"furthestStage": 20,
@@ -60,7 +60,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: recent fallthrough at
}
`;
exports[`W7 snapshots heat at canonical inputs > heat: typical mid-funnel hot lead 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: typical mid-funnel hot lead 1`] = `
{
"eoiCount": 10,
"furthestStage": 30,
@@ -70,7 +70,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: typical mid-funnel ho
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=0, lost=0, stage=0) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=0, lost=0, stage=0) is stable 1`] = `
{
"in": {
"activeInterestCount": 0,
@@ -81,7 +81,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=0, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=0, lost=1, stage=0) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=0, lost=1, stage=0) is stable 1`] = `
{
"in": {
"activeInterestCount": 0,
@@ -92,7 +92,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=0, lost=1, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=0, lost=5, stage=0) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=0, lost=5, stage=0) is stable 1`] = `
{
"in": {
"activeInterestCount": 0,
@@ -103,7 +103,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=0, lost=5, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=1, lost=0, stage=1) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=1, lost=0, stage=1) is stable 1`] = `
{
"in": {
"activeInterestCount": 1,
@@ -114,7 +114,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=1, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=1, lost=0, stage=3) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=1, lost=0, stage=3) is stable 1`] = `
{
"in": {
"activeInterestCount": 1,
@@ -125,7 +125,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=1, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=1, lost=0, stage=4) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=1, lost=0, stage=4) is stable 1`] = `
{
"in": {
"activeInterestCount": 1,
@@ -136,7 +136,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=1, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=1, lost=0, stage=5) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=1, lost=0, stage=5) is stable 1`] = `
{
"in": {
"activeInterestCount": 1,
@@ -147,7 +147,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=1, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=1, lost=0, stage=6) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=1, lost=0, stage=6) is stable 1`] = `
{
"in": {
"activeInterestCount": 1,
@@ -158,7 +158,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=1, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=1, lost=5, stage=6) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=1, lost=5, stage=6) is stable 1`] = `
{
"in": {
"activeInterestCount": 1,
@@ -169,7 +169,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=1, lost=5, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=2, lost=0, stage=5) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=2, lost=0, stage=5) is stable 1`] = `
{
"in": {
"activeInterestCount": 2,
@@ -180,7 +180,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=2, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=3, lost=2, stage=4) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=3, lost=2, stage=4) is stable 1`] = `
{
"in": {
"activeInterestCount": 3,

View File

@@ -39,7 +39,7 @@ async function buildAcroFormPdf(): Promise<Buffer> {
return Buffer.from(bytes);
}
describe('parseBerthPdf AcroForm tier', () => {
describe('parseBerthPdf - AcroForm tier', () => {
it('extracts named fields and skips OCR', async () => {
const buf = await buildAcroFormPdf();
const result = await parseBerthPdf(buf, { skipOcr: true });

View File

@@ -1,5 +1,5 @@
/**
* Unit tests for the berth PDF parser (Phase 6b see plan §4.7b, §14.6).
* Unit tests for the berth PDF parser (Phase 6b - see plan §4.7b, §14.6).
*
* Covers:
* - Magic-byte check (`%PDF-`).
@@ -58,7 +58,7 @@ describe('parseHumanDate', () => {
});
});
describe('extractFromOcrText sample berth A1', () => {
describe('extractFromOcrText - sample berth A1', () => {
// Mirrors the layout of Berth_Spec_Sheet_A1.pdf documented in plan §9.2.
const sample = `
PORT NIMARA
@@ -144,7 +144,7 @@ Access: Car to Vessel (max. 3 ton)
});
});
describe('extractFromOcrText imperial/metric drift warning', () => {
describe('extractFromOcrText - imperial/metric drift warning', () => {
it('flags a >1% mismatch', () => {
const { warnings } = extractFromOcrText('Length: 100 ft / 50m');
expect(warnings.some((w) => /mismatch/i.test(w))).toBe(true);

View File

@@ -188,12 +188,12 @@ describe('computeHeat', () => {
});
});
// ─── W7 snapshot lockfile locks current tier-ladder boundaries and heat
// ─── W7 snapshot lockfile - locks current tier-ladder boundaries and heat
// ordering so weight-tuning changes can't silently shift outputs. The
// existing toBe / toBeCloseTo tests above cover correctness; these
// inline snapshots are the regression-catching tripwires.
describe('W7 snapshots tier-ladder boundaries', () => {
describe('W7 snapshots - tier-ladder boundaries', () => {
it.each([
[0, 0, 0],
[0, 1, 0],
@@ -217,7 +217,7 @@ describe('W7 snapshots — tier-ladder boundaries', () => {
);
});
describe('W7 snapshots heat at canonical inputs', () => {
describe('W7 snapshots - heat at canonical inputs', () => {
const NOW = new Date('2026-05-05T00:00:00Z');
const w = DEFAULT_RECOMMENDER_SETTINGS;
@@ -238,7 +238,7 @@ describe('W7 snapshots — heat at canonical inputs', () => {
w,
NOW,
);
// Snapshot the rounded breakdown exact float math (toBeCloseTo)
// Snapshot the rounded breakdown - exact float math (toBeCloseTo)
// is covered above; this locks the relative ordering + magnitude.
expect({
total: Math.round(h.total * 1000) / 1000,

View File

@@ -20,7 +20,7 @@ import { companyMemberships } from '@/lib/db/schema/companies';
// ─── createPending ───────────────────────────────────────────────────────────
describe('berth-reservations.service createPending', () => {
describe('berth-reservations.service - createPending', () => {
it('creates pending reservation for client-owned yacht', async () => {
const port = await makePort();
const berth = await makeBerth({ portId: port.id });
@@ -203,7 +203,7 @@ describe('berth-reservations.service — createPending', () => {
// ─── Lifecycle transitions ───────────────────────────────────────────────────
describe('berth-reservations.service lifecycle transitions', () => {
describe('berth-reservations.service - lifecycle transitions', () => {
async function setup() {
const port = await makePort();
const berth = await makeBerth({ portId: port.id });
@@ -330,7 +330,7 @@ describe('berth-reservations.service — lifecycle transitions', () => {
// ─── listReservations ────────────────────────────────────────────────────────
describe('berth-reservations.service listReservations', () => {
describe('berth-reservations.service - listReservations', () => {
async function makeReservation(portId: string, opts?: { berthId?: string }) {
const berth = opts?.berthId ? { id: opts.berthId } : await makeBerth({ portId });
const client = await makeClient({ portId });
@@ -418,7 +418,7 @@ describe('berth-reservations.service — listReservations', () => {
// ─── Self-check: DB state is as expected after cancel ────────────────────────
describe('berth-reservations.service DB state', () => {
describe('berth-reservations.service - DB state', () => {
it('cancel persists status=cancelled in the database', async () => {
const port = await makePort();
const berth = await makeBerth({ portId: port.id });

View File

@@ -14,7 +14,7 @@ import { companies } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { ConflictError, NotFoundError } from '@/lib/errors';
describe('companies.service createCompany', () => {
describe('companies.service - createCompany', () => {
it('creates a company with minimal required fields', async () => {
const port = await makePort();
@@ -68,7 +68,7 @@ describe('companies.service — createCompany', () => {
});
});
describe('companies.service upsertByName', () => {
describe('companies.service - upsertByName', () => {
it('returns existing company on case-insensitive match', async () => {
const port = await makePort();
const original = await createCompany(
@@ -109,7 +109,7 @@ describe('companies.service — upsertByName', () => {
});
});
describe('companies.service updateCompany', () => {
describe('companies.service - updateCompany', () => {
it('updates fields', async () => {
const port = await makePort();
const company = await makeCompany({
@@ -147,7 +147,7 @@ describe('companies.service — updateCompany', () => {
});
});
describe('companies.service archiveCompany', () => {
describe('companies.service - archiveCompany', () => {
it('sets archivedAt to a non-null timestamp', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });
@@ -173,7 +173,7 @@ describe('companies.service — archiveCompany', () => {
});
});
describe('companies.service listCompanies', () => {
describe('companies.service - listCompanies', () => {
it('is tenant-scoped', async () => {
const portA = await makePort();
const portB = await makePort();
@@ -229,7 +229,7 @@ describe('companies.service — listCompanies', () => {
});
});
describe('companies.service autocomplete', () => {
describe('companies.service - autocomplete', () => {
it('matches by name', async () => {
const port = await makePort();
await makeCompany({ portId: port.id, overrides: { name: 'Phoenix Ltd' } });
@@ -248,7 +248,7 @@ describe('companies.service — autocomplete', () => {
});
});
describe('companies.service getCompanyById', () => {
describe('companies.service - getCompanyById', () => {
it('returns the company when same tenant', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });

View File

@@ -13,7 +13,7 @@ import { companyMemberships } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
describe('company-memberships.service addMembership', () => {
describe('company-memberships.service - addMembership', () => {
it('creates a membership for a valid client + company in the same port', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });
@@ -122,7 +122,7 @@ describe('company-memberships.service — addMembership', () => {
});
});
describe('company-memberships.service updateMembership', () => {
describe('company-memberships.service - updateMembership', () => {
it('updates fields', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });
@@ -180,7 +180,7 @@ describe('company-memberships.service — updateMembership', () => {
});
});
describe('company-memberships.service setPrimary', () => {
describe('company-memberships.service - setPrimary', () => {
it('sets only one membership as primary per company (atomic un-primary others)', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });
@@ -222,7 +222,7 @@ describe('company-memberships.service — setPrimary', () => {
makeAuditMeta({ portId: port.id }),
);
// Mark all primary via the service only the last call should leave a
// Mark all primary via the service - only the last call should leave a
// single primary survivor (m3).
await setPrimary(m1.id, port.id, makeAuditMeta({ portId: port.id }));
await setPrimary(m2.id, port.id, makeAuditMeta({ portId: port.id }));
@@ -262,7 +262,7 @@ describe('company-memberships.service — setPrimary', () => {
});
});
describe('company-memberships.service endMembership', () => {
describe('company-memberships.service - endMembership', () => {
it('sets endDate', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });
@@ -293,7 +293,7 @@ describe('company-memberships.service — endMembership', () => {
});
});
describe('company-memberships.service listByCompany / listByClient', () => {
describe('company-memberships.service - listByCompany / listByClient', () => {
it('returns active memberships only by default', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });

View File

@@ -97,7 +97,7 @@ describe('buildDocumensoPayload', () => {
Length: '45 ft',
Width: '14 ft',
Draft: '6 ft',
// Berth Number carries the formatBerthRange output single-
// Berth Number carries the formatBerthRange output - single-
// berth EOI duplicates the primary mooring; multi-berth shows
// the compact range. The separate 'Berth Range' formValue key
// was retired 2026-05-14 (the Documenso template never had
@@ -134,7 +134,7 @@ describe('buildDocumensoPayload', () => {
it('renders empty Section 3 when yacht and berth are not linked', () => {
// Also explicitly clear the berth-range fallback that defaults to
// the primary mooring when there's no berth AND no bundle, the
// the primary mooring - when there's no berth AND no bundle, the
// form field renders as empty.
const ctx = makeContext({ yacht: null, berth: null, eoiBerthRange: '' });
const payload = buildDocumensoPayload(ctx, OPTIONS);

View File

@@ -115,7 +115,11 @@ describe('placeFields v2 dispatch', () => {
'env-123',
[
{
recipientId: 'rec-a',
// v2 recipient ids are numeric - Documenso's distribute response
// returns them as numbers. The CRM custom-document-upload
// service preserves them as strings or numbers; the v2 placeFields
// coercion normalises to number for the upstream payload.
recipientId: '42',
type: 'SIGNATURE',
pageNumber: 1,
pageX: 25,
@@ -134,9 +138,13 @@ describe('placeFields v2 dispatch', () => {
expect((init as RequestInit).method).toBe('POST');
const body = JSON.parse(String((init as RequestInit).body)) as any;
expect(body.envelopeId).toBe('env-123');
expect(body.fields[0]).toMatchObject({
recipientId: 'rec-a',
// 2026-05-22: Documenso v2 expects the array under `data` (trpc-style
// createMany input), not `fields`. recipientId is a number, and the
// page-index key is `page` (not `pageNumber`).
expect(body.data[0]).toMatchObject({
recipientId: 42,
type: 'SIGNATURE',
page: 1,
positionX: 25,
positionY: 88,
width: 20,
@@ -262,17 +270,22 @@ describe('placeDefaultSignatureFields integration', () => {
await placeDefaultSignatureFields(
'env-x',
[
{ id: 'r1', pageNumber: 4 },
{ id: 'r2', pageNumber: 4 },
{ id: 'r3', pageNumber: 4 },
{ id: '101', pageNumber: 4 },
{ id: '102', pageNumber: 4 },
{ id: '103', pageNumber: 4 },
],
'port-1',
);
expect(fetchMock).toHaveBeenCalledTimes(1);
const body = JSON.parse(String((fetchMock.mock.calls[0]![1] as RequestInit).body)) as any;
expect(body.fields).toHaveLength(3);
expect(body.fields.every((f: { type: string }) => f.type === 'SIGNATURE')).toBe(true);
expect(body.fields.every((f: { pageNumber: number }) => f.pageNumber === 4)).toBe(true);
// 2026-05-22: Documenso v2 expects `data` (not `fields`), `page`
// (not `pageNumber`), and numeric recipientIds.
expect(body.data).toHaveLength(3);
expect(body.data.every((f: { type: string }) => f.type === 'SIGNATURE')).toBe(true);
expect(body.data.every((f: { page: number }) => f.page === 4)).toBe(true);
expect(
body.data.every((f: { recipientId: unknown }) => typeof f.recipientId === 'number'),
).toBe(true);
});
it('skips the API call entirely with zero recipients', async () => {

View File

@@ -32,7 +32,7 @@ describe('extractSigningToken', () => {
});
it('rejects path tails with disallowed characters', () => {
// Real tokens are URL-safe base64 no spaces, no punctuation
// Real tokens are URL-safe base64 - no spaces, no punctuation
expect(extractSigningToken('https://example.com/sign/has%20space')).toBeNull();
});

View File

@@ -3,13 +3,13 @@ import { describe, it, expect } from 'vitest';
import { transformSigningUrl } from '@/lib/services/document-signing-emails.service';
/**
* Phase 5 pin the URL-wrapping contract.
* Phase 5 - pin the URL-wrapping contract.
*
* The marketing website at portnimara.com/sign/[type]/[token] expects
* specific path segments (`client | cc | developer | witness`) and the
* Documenso webhook returns raw URLs of the form
* `https://signatures.portnimara.com/sign/<token>`. transformSigningUrl
* is the seam between the two these tests guard the role-to-URL-
* is the seam between the two - these tests guard the role-to-URL-
* segment mapping so a future refactor can't silently break the
* embedded signing pages.
*/
@@ -38,7 +38,7 @@ describe('transformSigningUrl', () => {
);
});
it('maps approver → /sign/cc/<token> website only handles {client, cc, developer, witness}', () => {
it('maps approver → /sign/cc/<token> - website only handles {client, cc, developer, witness}', () => {
expect(transformSigningUrl(RAW, HOST, 'approver')).toBe(
'https://portnimara.com/sign/cc/vbT8hi3jKQmrFP_LN1WcS',
);
@@ -50,7 +50,7 @@ describe('transformSigningUrl', () => {
);
});
it('maps other → /sign/cc/<token> funnels through CC page with passive copy', () => {
it('maps other → /sign/cc/<token> - funnels through CC page with passive copy', () => {
expect(transformSigningUrl(RAW, HOST, 'other')).toBe(
'https://portnimara.com/sign/cc/vbT8hi3jKQmrFP_LN1WcS',
);
@@ -65,7 +65,7 @@ describe('transformSigningUrl', () => {
);
});
it('preserves the token verbatim no URL encoding / re-shaping', () => {
it('preserves the token verbatim - no URL encoding / re-shaping', () => {
const odd = 'https://sig.example.com/sign/Aa_-Zz09_-XYZ';
expect(transformSigningUrl(odd, HOST, 'developer')).toBe(
'https://portnimara.com/sign/developer/Aa_-Zz09_-XYZ',

View File

@@ -305,7 +305,7 @@ describe('buildEoiContext', () => {
it('throws ValidationError when client has no email', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });
// Address only, no email gate should fail.
// Address only, no email - gate should fail.
await db.insert(clientAddresses).values({
clientId: client.id,
portId: port.id,
@@ -324,7 +324,7 @@ describe('buildEoiContext', () => {
it('throws ValidationError when client has no primary address', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });
// Email only, no address gate should fail.
// Email only, no address - gate should fail.
await db.insert(clientContacts).values({
clientId: client.id,
channel: 'email',

View File

@@ -1,7 +1,6 @@
import { describe, it, expect, beforeAll } from 'vitest';
describe('portal.service getPortalUserYachts', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
describe('portal.service - getPortalUserYachts', () => {
let getPortalUserYachts: (clientId: string, portId: string) => Promise<Array<any>>;
let makeClient: typeof import('../../helpers/factories').makeClient;
@@ -87,7 +86,7 @@ describe('portal.service — getPortalUserYachts', () => {
// defensive by forcing the yacht's current owner to company after the
// direct query path has already cached it. We simulate the case by
// creating a client-owned yacht, then manually flipping owner to a
// company the client is a member of if both queries ran they'd both
// company the client is a member of - if both queries ran they'd both
// match, but dedup by id ensures only one entry.
const port = await makePort();
const client = await makeClient({ portId: port.id });
@@ -104,7 +103,7 @@ describe('portal.service — getPortalUserYachts', () => {
ownerId: client.id,
name: 'Ambiguous',
});
// Flip the denormalized owner to the company (without updating history)
// Flip the denormalized owner to the company (without updating history) -
// this is artificial but exercises the dedup branch.
await db
.update(yachts)
@@ -142,7 +141,7 @@ describe('portal.service — getPortalUserYachts', () => {
const portA = await makePort();
const portB = await makePort();
const clientInA = await makeClient({ portId: portA.id });
// Directly-owned yacht in portB with the SAME client id must not leak
// Directly-owned yacht in portB with the SAME client id - must not leak
// because getPortalUserYachts filters on portId.
// We insert a yacht row in portB with ownerId=clientInA.id. The FK on
// yachts.currentOwnerId isn't to clients, so this is valid.
@@ -172,8 +171,7 @@ describe('portal.service — getPortalUserYachts', () => {
});
});
describe('portal.service getPortalUserMemberships', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
describe('portal.service - getPortalUserMemberships', () => {
let getPortalUserMemberships: (clientId: string, portId: string) => Promise<Array<any>>;
let makeClient: typeof import('../../helpers/factories').makeClient;
@@ -223,7 +221,7 @@ describe('portal.service — getPortalUserMemberships', () => {
const portA = await makePort();
const portB = await makePort();
const client = await makeClient({ portId: portA.id });
// Company in portB but membership references clientId on portA.
// Company in portB - but membership references clientId on portA.
const companyInB = await makeCompany({ portId: portB.id });
await makeMembership({
companyId: companyInB.id,
@@ -240,8 +238,7 @@ describe('portal.service — getPortalUserMemberships', () => {
});
});
describe('portal.service getPortalUserReservations', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
describe('portal.service - getPortalUserReservations', () => {
let getPortalUserReservations: (clientId: string, portId: string) => Promise<Array<any>>;
let makeClient: typeof import('../../helpers/factories').makeClient;

View File

@@ -91,7 +91,7 @@ describe('computeTotalForecast', () => {
],
{ enquiry: 0.3 },
);
// 'unknown_stage' canonicalizes to 'enquiry' (fallback) so it ALSO
// 'unknown_stage' canonicalizes to 'enquiry' (fallback) - so it ALSO
// gets the enquiry weight. Verifies canonicalization stays consistent
// between rollup and forecast so the totals reconcile.
// 1000*0.3 + 2000*0.3 = 300 + 600 = 900

View File

@@ -7,13 +7,13 @@ import { yachts, companies } from '@/lib/db/schema';
import { makePort, makeClient, makeYacht, makeCompany } from '../../helpers/factories';
// Default opts super admin so every bucket runs without per-resource
// Default opts - super admin so every bucket runs without per-resource
// permission gating getting in the way of the assertions.
const ADMIN_OPTS = { permissions: null, isSuperAdmin: true } as const;
// ─── Yachts ──────────────────────────────────────────────────────────────────
describe('search.service yachts', () => {
describe('search.service - yachts', () => {
it('matches yachts by name (case-insensitive)', async () => {
const port = await makePort();
const owner = await makeClient({ portId: port.id });
@@ -106,7 +106,7 @@ describe('search.service — yachts', () => {
// ─── Companies ───────────────────────────────────────────────────────────────
describe('search.service companies', () => {
describe('search.service - companies', () => {
it('matches companies by name', async () => {
const port = await makePort();
await makeCompany({ portId: port.id, overrides: { name: 'Poseidon Maritime Ltd' } });
@@ -165,7 +165,7 @@ describe('search.service — companies', () => {
// ─── Combined ────────────────────────────────────────────────────────────────
describe('search.service combined', () => {
describe('search.service - combined', () => {
it('returns clients, yachts, and companies for a query that matches multiple', async () => {
const port = await makePort();
const client = await makeClient({
@@ -242,7 +242,7 @@ describe('normalizePhoneQuery', () => {
// ─── Partial name matching ───────────────────────────────────────────────────
describe('search.service partial name matching', () => {
describe('search.service - partial name matching', () => {
it('matches "joh smi" against "John Smith" via tokenized prefix tsquery', async () => {
const port = await makePort();
await makeClient({ portId: port.id, overrides: { fullName: 'John Smith' } });
@@ -263,7 +263,7 @@ describe('search.service — partial name matching', () => {
});
});
describe('search.service bucket totals', () => {
describe('search.service - bucket totals', () => {
it('emits per-bucket totals so the UI can render "show more" links', async () => {
const port = await makePort();
await makeClient({ portId: port.id, overrides: { fullName: 'TotalsCheck One' } });

View File

@@ -18,7 +18,7 @@ import { db } from '@/lib/db';
import { yachts, yachtOwnershipHistory } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
describe('yachts.service createYacht', () => {
describe('yachts.service - createYacht', () => {
it('creates a yacht with a client owner and opens an ownership history row', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });
@@ -79,7 +79,7 @@ describe('yachts.service — createYacht', () => {
});
});
describe('yachts.service updateYacht', () => {
describe('yachts.service - updateYacht', () => {
it('updates name and notes', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });
@@ -144,7 +144,7 @@ describe('yachts.service — updateYacht', () => {
});
});
describe('yachts.service archiveYacht', () => {
describe('yachts.service - archiveYacht', () => {
it('sets archivedAt to a non-null timestamp', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });
@@ -174,7 +174,7 @@ describe('yachts.service — archiveYacht', () => {
});
});
describe('yachts.service listYachts', () => {
describe('yachts.service - listYachts', () => {
it('is scoped to port (tenant isolation)', async () => {
const portA = await makePort();
const portB = await makePort();
@@ -269,7 +269,7 @@ describe('yachts.service — listYachts', () => {
});
});
describe('yachts.service listYachtsForOwner', () => {
describe('yachts.service - listYachtsForOwner', () => {
it('returns all yachts owned by a given client', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });
@@ -305,7 +305,7 @@ describe('yachts.service — listYachtsForOwner', () => {
});
});
describe('yachts.service autocomplete', () => {
describe('yachts.service - autocomplete', () => {
it('matches by name (ILIKE)', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });

View File

@@ -17,7 +17,7 @@ import { tmpdir } from 'node:os';
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
// Stub the env module BEFORE importing the backend so the
// MULTI_NODE_DEPLOYMENT toggle works env is now read from the zod
// MULTI_NODE_DEPLOYMENT toggle works - env is now read from the zod
// schema once at module load, not from process.env at runtime.
vi.mock('@/lib/env', async () => {
const actual = await vi.importActual<typeof import('@/lib/env')>('@/lib/env');
@@ -102,7 +102,7 @@ describe('FilesystemBackend realpath check', () => {
// realpath'd storage root. Note: Node's path.resolve doesn't follow
// symlinks; the runtime guard relies on the resolved target string staying
// under rootResolved. Since the symlink itself lives under root, path.resolve
// would produce <root>/evil/file.txt which IS under root by string check.
// would produce <root>/evil/file.txt - which IS under root by string check.
// The defense-in-depth here is that the storage root itself is realpath'd
// at create time, AND the OS perms (0o700) limit lateral movement. We assert
// the obvious traversal attack still fails.
@@ -140,7 +140,7 @@ describe('FilesystemBackend realpath check', () => {
const prev = env.MULTI_NODE_DEPLOYMENT;
// The backend reads env.MULTI_NODE_DEPLOYMENT (zod-validated, set
// once at module load). Mutate the in-memory env for the duration of
// this case the surrounding vi.mock() above keeps every other env
// this case - the surrounding vi.mock() above keeps every other env
// field intact.
(env as unknown as { MULTI_NODE_DEPLOYMENT: boolean }).MULTI_NODE_DEPLOYMENT = true;
try {

View File

@@ -401,7 +401,7 @@ describe('updateFieldSchema', () => {
});
it('does NOT accept fieldType (immutability by omission)', () => {
// fieldType is omitted from the schema it should be stripped or cause a strict failure
// fieldType is omitted from the schema - it should be stripped or cause a strict failure
// With Zod default (strip mode), unknown keys are stripped and parse succeeds.
// The important check is that the parsed output does NOT include fieldType.
const result = updateFieldSchema.safeParse({ fieldType: 'number' });

View File

@@ -8,7 +8,7 @@ const baseInput = {
bodyHtml: '<p>x</p>',
};
describe('createTemplateSchema mergeFields allow-list', () => {
describe('createTemplateSchema - mergeFields allow-list', () => {
it('accepts valid tokens from the catalog', () => {
const parsed = createTemplateSchema.parse({
...baseInput,

View File

@@ -38,13 +38,13 @@ describe('isLocalOrPrivateHost', () => {
'https://api.example.com/webhook',
'https://1.1.1.1/x', // public DNS
'https://8.8.8.8/x', // public DNS
'https://203.0.113.5/x', // TEST-NET-3 documentation range public
'https://203.0.113.5/x', // TEST-NET-3 documentation range - public
])('allows %s', (url) => {
expect(isLocalOrPrivateHost(url)).toBe(false);
});
it('returns true for malformed URLs (fail closed)', () => {
expect(isLocalOrPrivateHost('not a url')).toBe(true);
expect(isLocalOrPrivateHost('javascript:alert(1)')).toBe(false); // parses, hostname empty but hostname check below catches
expect(isLocalOrPrivateHost('javascript:alert(1)')).toBe(false); // parses, hostname empty - but hostname check below catches
});
});

View File

@@ -49,7 +49,7 @@ function makeReq(body: unknown, headers: Record<string, string> = {}) {
} as unknown as import('next/server').NextRequest;
}
describe('POST /api/public/website-inquiries 503 when secret unset', () => {
describe('POST /api/public/website-inquiries - 503 when secret unset', () => {
it('returns 503 even when a "valid" header + payload are supplied', async () => {
const { POST } = await import('@/app/api/public/website-inquiries/route');
const res = await POST(

View File

@@ -1,5 +1,5 @@
/**
* /api/public/website-inquiries route unit tests.
* /api/public/website-inquiries route - unit tests.
*
* Asserts:
* 1. Auth: rejects missing/wrong X-Webhook-Secret with 401.
@@ -22,7 +22,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const VALID_UUID = '11111111-1111-4111-8111-111111111111';
const SECRET = 'test-secret-at-least-16-chars-long';
// ─── Mock state module-scoped so test-level mutations are visible ────
// ─── Mock state - module-scoped so test-level mutations are visible ────
interface MockState {
portRow: Array<{ id: string }>;
@@ -43,7 +43,7 @@ const state: MockState = {
queryCount: 0,
};
// ─── Hoisted mocks apply for the entire file ────────────────────────
// ─── Hoisted mocks - apply for the entire file ────────────────────────
vi.mock('@/lib/env', () => ({
env: { WEBSITE_INTAKE_SECRET: SECRET },
@@ -134,7 +134,7 @@ afterEach(() => {
// ─── Tests ────────────────────────────────────────────────────────────
describe('POST /api/public/website-inquiries auth + capture', () => {
describe('POST /api/public/website-inquiries - auth + capture', () => {
it('returns 401 when the X-Webhook-Secret header is missing', async () => {
const { POST } = await import('@/app/api/public/website-inquiries/route');
const res = await POST(makeReq({}));