chore: prettier formatter drift across recent commits

Prettier reformatting on files touched in the wave 11.B sequence —
markdown italics _underscore-style_, single-line conditionals, minor
whitespace fixes. No semantic changes. .env.example reformatting left
unstaged (blocked by pre-commit hook).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 10:57:37 +02:00
parent 286eb51f81
commit 5422f11747
10 changed files with 82 additions and 95 deletions

View File

@@ -8,27 +8,27 @@
Working on branch `feat/documents-folders` (off `main`). Subagent-driven execution: every task gets implementer → spec reviewer → code-quality reviewer → fix loop if needed.
| Task | Topic | Status | Commit(s) |
|------|-------|--------|-----------|
| 1 | Schema + migration (`document_folders` + `folder_id` on documents) | ✅ Done | `5bed62d` + `4a50bab` (fix: Drizzle `.references()` + relations) |
| 2 | `documents.manage_folders` permission | ✅ Done | `e6cf50f` |
| 3 | Service: `listTree` + `createFolder` (TDD) | ✅ Done | `4b31f01` + `5c5ab49` (fix: port-scope test cleanup + tighten message) |
| 4 | Service: rename + move (cycle prevention) + soft-rescue delete | ✅ Done | `e9251a3` + `4ec0004` (fix: audit-log out of tx + portId on ancestor walk + drop misleading updatedAt + userId for rename/move audit) |
| 5 | Zod validators | ✅ Done | `830ac39` |
| 6 | Folder API routes (GET tree / POST / PATCH rename-or-move / DELETE) | ✅ Done | `1082b80` + `e9d5df6` (fix: `.strict()` on union members so `{name, parentId}` together is a 400 not silent drop) |
| 7 | listDocuments folder filter + per-doc move route | ✅ Done | `a0ffa1b` |
| 8 | `useDocumentFolders` hook | 🔴 Not started | — |
| 9 | `FolderTreeSidebar` component | 🔴 Not started | — |
| 10 | `FolderBreadcrumb` component | 🔴 Not started | — |
| 11 | `FolderActionsMenu` (create/rename/delete dialogs) | 🔴 Not started | — |
| 12 | `MoveToFolderDialog` (per-doc picker) | 🔴 Not started | — |
| 13 | Wire `DocumentsHub`: sidebar + breadcrumb, drop signature pill, In-progress tab | 🔴 Not started | — |
| 14 | Dynamic type-filter chips + per-row Move action | 🔴 Not started | — |
| 15 | Admin-configurable Expired tab | 🔴 Not started | — |
| 16 | Playwright smoke test | 🔴 Not started | — |
| 17 | CLAUDE.md update + final verification | 🔴 Not started | — |
| 18 | **NEW** — path-style download URLs (hybrid storage decision) | 🔴 Not started | — |
| 19 | **NEW** — importer from organized S3/filesystem bucket | 🔴 Not started | — |
| Task | Topic | Status | Commit(s) |
| ---- | ------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Schema + migration (`document_folders` + `folder_id` on documents) | ✅ Done | `5bed62d` + `4a50bab` (fix: Drizzle `.references()` + relations) |
| 2 | `documents.manage_folders` permission | ✅ Done | `e6cf50f` |
| 3 | Service: `listTree` + `createFolder` (TDD) | ✅ Done | `4b31f01` + `5c5ab49` (fix: port-scope test cleanup + tighten message) |
| 4 | Service: rename + move (cycle prevention) + soft-rescue delete | ✅ Done | `e9251a3` + `4ec0004` (fix: audit-log out of tx + portId on ancestor walk + drop misleading updatedAt + userId for rename/move audit) |
| 5 | Zod validators | ✅ Done | `830ac39` |
| 6 | Folder API routes (GET tree / POST / PATCH rename-or-move / DELETE) | ✅ Done | `1082b80` + `e9d5df6` (fix: `.strict()` on union members so `{name, parentId}` together is a 400 not silent drop) |
| 7 | listDocuments folder filter + per-doc move route | ✅ Done | `a0ffa1b` |
| 8 | `useDocumentFolders` hook | 🔴 Not started | — |
| 9 | `FolderTreeSidebar` component | 🔴 Not started | — |
| 10 | `FolderBreadcrumb` component | 🔴 Not started | — |
| 11 | `FolderActionsMenu` (create/rename/delete dialogs) | 🔴 Not started | — |
| 12 | `MoveToFolderDialog` (per-doc picker) | 🔴 Not started | — |
| 13 | Wire `DocumentsHub`: sidebar + breadcrumb, drop signature pill, In-progress tab | 🔴 Not started | — |
| 14 | Dynamic type-filter chips + per-row Move action | 🔴 Not started | — |
| 15 | Admin-configurable Expired tab | 🔴 Not started | — |
| 16 | Playwright smoke test | 🔴 Not started | — |
| 17 | CLAUDE.md update + final verification | 🔴 Not started | — |
| 18 | **NEW** — path-style download URLs (hybrid storage decision) | 🔴 Not started | — |
| 19 | **NEW** — importer from organized S3/filesystem bucket | 🔴 Not started | — |
**Test posture at pause:** `pnpm exec tsc --noEmit` clean; full vitest suite **1213/1213 passing** (108 test files). 11 commits on the branch ahead of `main`.

View File

@@ -20,7 +20,7 @@ This spec unifies both surfaces under a single hub with a stacked **Signing in p
Three first-class concepts after this spec ships:
- **File** (`files` row) — a stored binary artifact (PDF/image/etc.) with one `folder_id` and entity FKs (`client_id` / `company_id` / `yacht_id`). The canonical "document" reps file and find. Produced by either direct upload or as the output of a completed signing workflow.
- **Signing workflow** (`documents` row) — the *process* of getting a PDF signed via Documenso. Lifecycle `draft``sent``partially_signed``completed`. Surfaces in the hub's Signing section while in-flight. On completion, produces a signed-PDF file; the workflow row itself becomes audit history accessed via a "view signing details" link on the resulting file. Stops appearing in user-facing folder views.
- **Signing workflow** (`documents` row) — the _process_ of getting a PDF signed via Documenso. Lifecycle `draft``sent``partially_signed``completed`. Surfaces in the hub's Signing section while in-flight. On completion, produces a signed-PDF file; the workflow row itself becomes audit history accessed via a "view signing details" link on the resulting file. Stops appearing in user-facing folder views.
- **Folder** (`document_folders` row) — per-port nestable tree (existing). Extended to hold both files and in-flight workflows. Gains three system-managed roots and per-entity auto-subfolders.
`documents.folder_id` stays meaningful for in-flight workflows (rep can file by deal/project). Becomes irrelevant on completion — the rendering layer hides completed workflows from folder views entirely.
@@ -56,7 +56,7 @@ Three first-class concepts after this spec ships:
- Bulk file actions (multi-select move, multi-select download zip) — separate work
- Tagging or labels on files — separate work
- Trash / restore for hard-deleted files (current behavior preserved)
- Search across file *content* (full-text PDF search) — current behavior preserved (search is title/filename only)
- Search across file _content_ (full-text PDF search) — current behavior preserved (search is title/filename only)
- Per-port admin override for aggregation symmetry (rejected as needless setting at E11)
- Per-user feature flag rollout — hard cutover (E rollout decision)
- Native PDF preview rebuild — existing `FilePreviewDialog` reused
@@ -78,7 +78,7 @@ Per-entity subfolders are created **lazily on first need** — when a workflow c
Subfolder naming:
- Default name = entity display name (client `firstName lastName` / company `name` / yacht `name`).
- Numeric collision suffix: `Smith, John (2)`, `Smith, John (3)`, etc. Suffix appended to the *new* (later-created) folder; existing folder names never change due to collision.
- Numeric collision suffix: `Smith, John (2)`, `Smith, John (3)`, etc. Suffix appended to the _new_ (later-created) folder; existing folder names never change due to collision.
- Auto-rename on entity rename — runs in the same DB transaction as the entity update.
- Entity archive: `(archived)` suffix appended, folder shown muted in tree, auto-deposit blocked until restored.
- Entity hard-delete: `(deleted)` suffix appended, `system_managed` flipped to `false` (folder demoted to a regular user folder; rep can rename/move/delete normally).
@@ -152,7 +152,7 @@ Aggregation is **symmetric** (E aggregation reach decision). Walking from any en
- linked clients via `company_memberships`
- linked companies via `company_memberships` and via yacht ownership
- linked yachts via current ownership (`yachts.current_owner_type` + `current_owner_id`)
- + any second-degree links (e.g., `Clients/Smith` shows files of `Smith Marine LLC`'s yachts via the chain Smith → Smith Marine LLC → owned yachts)
- - any second-degree links (e.g., `Clients/Smith` shows files of `Smith Marine LLC`'s yachts via the chain Smith → Smith Marine LLC → owned yachts)
Each result group is rendered with a labelled header: `DIRECTLY ATTACHED · 3`, `FROM COMPANY — SMITH MARINE LLC · 1`, `FROM YACHT — MV SERENITY · 2`, etc. Files lived where they were physically filed (e.g., `Yachts/MV Serenity/`); the aggregation only borrows them for display, with a `lives in <path>` caption per row.
@@ -227,26 +227,26 @@ Today's signing-status tabs (`in_progress` / `eoi_queue` / `awaiting_them` / `aw
## Edge cases — decisions
| ID | Edge case | Decision |
|----|-----------|----------|
| E1 | Entity renamed | System folder name auto-syncs in the same transaction. |
| E2 | Two entities collide on folder name (e.g., both "Smith, John") | Append numeric suffix `(2)`, `(3)` to the **new** colliding folder. Existing folders never change. |
| E3 | Entity archived | Folder stays with `(archived)` suffix, muted style. Auto-deposit halts. |
| E4 | Entity hard-deleted | Folder gets `(deleted)` suffix, `system_managed` flips to `false` (rep can clean up). Files retain orphaned data. |
| E5 | Yacht ownership transferred | Files snapshot their entity FKs at upload time. Old client folder retains historical files; new client gets a new folder for files created after transfer. Both coexist. |
| E6 | Workflow's owner FK changes mid-signing | Resolve owner at completion time. Signed PDF lands in current owner's folder. |
| E7 | Rep moves a file out of a system folder | Allowed. `folder_id` changes; entity FK is unchanged so aggregation still surfaces it via FK. The "lives in …" caption updates. |
| E8 | Rep manually uploads into an entity folder | Auto-set the file's matching entity FK from the destination folder's `entity_type` + `entity_id`. Custom folders → no auto-mapping. |
| E9 | Workflow has no entity at all | Signed PDF lands at root with `folder_id = null`. Surfaces in root-view Files section only. |
| E10 | File/workflow attached to interest only, interest has no resolved owner | Same as E9 — root, null folder. Manual move or future backfill resolves later. |
| E11 | Aggregated view returns 1000+ files | Top 20 per owner-source group, `Show all (N)` drilldown into flat paginated list per source. |
| E12 | Hub root view (no folder selected) | Port-wide Signing + recent Files, both paginated. |
| E13 | Concurrent completions race for the same entity folder | `INSERT … ON CONFLICT DO NOTHING RETURNING id`, then re-`SELECT` if needed. Uses the new partial unique index `uniq_document_folders_entity`. |
| E14 | Cross-port aggregation leak | `port_id = $p` filter at every join in aggregation SQL. Defense-in-depth. |
| Lazy folder creation | When are system root + per-entity folders created? | Roots: on port init. Subfolders: lazy on first need (auto-deposit, manual upload, or "Open folder" button on entity page). |
| Aggregation reach | Symmetric or owner-down only? | Symmetric — walk relationships in both directions. `Clients/Smith/`, `Companies/Smith Marine LLC/`, `Yachts/MV Serenity/` all show the full graph from their vantage point. |
| Search scope | Where does the search box look? | Current folder + descendants. Empty/root selection → port-wide. Includes both Signing and Files results. |
| Rollout | Feature flag or hard cutover? | Hard cutover. Migration backfills data; new hub replaces old hub on merge. |
| ID | Edge case | Decision |
| -------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| E1 | Entity renamed | System folder name auto-syncs in the same transaction. |
| E2 | Two entities collide on folder name (e.g., both "Smith, John") | Append numeric suffix `(2)`, `(3)` to the **new** colliding folder. Existing folders never change. |
| E3 | Entity archived | Folder stays with `(archived)` suffix, muted style. Auto-deposit halts. |
| E4 | Entity hard-deleted | Folder gets `(deleted)` suffix, `system_managed` flips to `false` (rep can clean up). Files retain orphaned data. |
| E5 | Yacht ownership transferred | Files snapshot their entity FKs at upload time. Old client folder retains historical files; new client gets a new folder for files created after transfer. Both coexist. |
| E6 | Workflow's owner FK changes mid-signing | Resolve owner at completion time. Signed PDF lands in current owner's folder. |
| E7 | Rep moves a file out of a system folder | Allowed. `folder_id` changes; entity FK is unchanged so aggregation still surfaces it via FK. The "lives in …" caption updates. |
| E8 | Rep manually uploads into an entity folder | Auto-set the file's matching entity FK from the destination folder's `entity_type` + `entity_id`. Custom folders → no auto-mapping. |
| E9 | Workflow has no entity at all | Signed PDF lands at root with `folder_id = null`. Surfaces in root-view Files section only. |
| E10 | File/workflow attached to interest only, interest has no resolved owner | Same as E9 — root, null folder. Manual move or future backfill resolves later. |
| E11 | Aggregated view returns 1000+ files | Top 20 per owner-source group, `Show all (N)` drilldown into flat paginated list per source. |
| E12 | Hub root view (no folder selected) | Port-wide Signing + recent Files, both paginated. |
| E13 | Concurrent completions race for the same entity folder | `INSERT … ON CONFLICT DO NOTHING RETURNING id`, then re-`SELECT` if needed. Uses the new partial unique index `uniq_document_folders_entity`. |
| E14 | Cross-port aggregation leak | `port_id = $p` filter at every join in aggregation SQL. Defense-in-depth. |
| Lazy folder creation | When are system root + per-entity folders created? | Roots: on port init. Subfolders: lazy on first need (auto-deposit, manual upload, or "Open folder" button on entity page). |
| Aggregation reach | Symmetric or owner-down only? | Symmetric — walk relationships in both directions. `Clients/Smith/`, `Companies/Smith Marine LLC/`, `Yachts/MV Serenity/` all show the full graph from their vantage point. |
| Search scope | Where does the search box look? | Current folder + descendants. Empty/root selection → port-wide. Includes both Signing and Files results. |
| Rollout | Feature flag or hard cutover? | Hard cutover. Migration backfills data; new hub replaces old hub on merge. |
## Schema deltas
@@ -361,13 +361,13 @@ Script: `pnpm tsx scripts/backfill-document-folders.ts`. Wraps in `pg_advisory_x
## Risks and mitigations
| Risk | Mitigation |
|------|------------|
| Aggregation queries slow on large portfolios (5k+ files per client) | Per-group pagination caps render cost; supporting indexes on `files(port_id, client_id)`, `files(port_id, company_id)`, `files(port_id, yacht_id)` already exist; new `files(folder_id)` and `files(port_id, folder_id)` cover folder filtering |
| Backfill migration locks production for too long | Per-port advisory lock; backfill batched in chunks of 1000 file rows per transaction; safe to re-run if interrupted |
| System-folder protection bypass via direct DB write | Application-level enforcement; we accept that direct DB writes can bypass (no DB constraint enforces "you can't update system_managed=true rows"). Audit log entries on folder ops surface anomalies |
| Hard cutover means broken hub if backfill fails | Backfill is idempotent and runs *before* code rollout; if backfill fails, the new hub still renders (just with sparse folders); rollback = revert the migration + redeploy old hub binary |
| Rep confused by "view signing details" link disappearing for non-Documenso signed files (e.g., manually uploaded "already signed" PDFs via /upload-signed) | The link shows only when `signed_file_id` traces to a `documents` row; manually-uploaded "signed" PDFs that bypass the workflow won't have the link, which is correct — there's nothing to show |
| Risk | Mitigation |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Aggregation queries slow on large portfolios (5k+ files per client) | Per-group pagination caps render cost; supporting indexes on `files(port_id, client_id)`, `files(port_id, company_id)`, `files(port_id, yacht_id)` already exist; new `files(folder_id)` and `files(port_id, folder_id)` cover folder filtering |
| Backfill migration locks production for too long | Per-port advisory lock; backfill batched in chunks of 1000 file rows per transaction; safe to re-run if interrupted |
| System-folder protection bypass via direct DB write | Application-level enforcement; we accept that direct DB writes can bypass (no DB constraint enforces "you can't update system_managed=true rows"). Audit log entries on folder ops surface anomalies |
| Hard cutover means broken hub if backfill fails | Backfill is idempotent and runs _before_ code rollout; if backfill fails, the new hub still renders (just with sparse folders); rollback = revert the migration + redeploy old hub binary |
| Rep confused by "view signing details" link disappearing for non-Documenso signed files (e.g., manually uploaded "already signed" PDFs via /upload-signed) | The link shows only when `signed_file_id` traces to a `documents` row; manually-uploaded "signed" PDFs that bypass the workflow won't have the link, which is correct — there's nothing to show |
## Open questions deferred to plan

View File

@@ -237,7 +237,9 @@ async function main(): Promise<void> {
console.log(`✓ Imported ${entry.key}`);
}
console.log(`\nDone. Created ${createdCount} documents, skipped ${skippedCount} (already imported).`);
console.log(
`\nDone. Created ${createdCount} documents, skipped ${skippedCount} (already imported).`,
);
}
async function ensureFolderChain(

View File

@@ -33,10 +33,7 @@ export const PATCH = withAuth(
if (body.folderId !== null) {
const folder = await db.query.documentFolders.findFirst({
where: and(
eq(documentFolders.id, body.folderId),
eq(documentFolders.portId, ctx.portId),
),
where: and(eq(documentFolders.id, body.folderId), eq(documentFolders.portId, ctx.portId)),
});
if (!folder) throw new ValidationError('Invalid folder');
}

View File

@@ -182,9 +182,7 @@ export function FolderActionsMenu({ selectedFolderId, onAfterDelete }: FolderAct
Cancel
</Button>
<Button
disabled={
!name.trim() || name.trim() === currentName || renameMutation.isPending
}
disabled={!name.trim() || name.trim() === currentName || renameMutation.isPending}
onClick={async () => {
try {
await renameMutation.mutateAsync({

View File

@@ -369,9 +369,7 @@ export class FilesystemBackend implements StorageBackend {
* caller doesn't have to special-case empty trees.
*/
async listByPrefix(prefix: string): Promise<string[]> {
const startAbs = prefix
? this.resolveKey(prefix.replace(/\/+$/, ''))
: this.rootResolved;
const startAbs = prefix ? this.resolveKey(prefix.replace(/\/+$/, '')) : this.rootResolved;
const out: string[] = [];
async function walk(dir: string): Promise<void> {
@@ -393,9 +391,7 @@ export class FilesystemBackend implements StorageBackend {
}
await walk(startAbs);
return out
.map((abs) => path.relative(this.rootResolved, abs).split(path.sep).join('/'))
.sort();
return out.map((abs) => path.relative(this.rootResolved, abs).split(path.sep).join('/')).sort();
}
/** Used by the proxy route — returns the validated absolute path. */

View File

@@ -124,13 +124,17 @@ describe('document-folders service · renameFolder', () => {
it('rejects rename to an existing sibling name', async () => {
await createFolder(portId, TEST_USER_ID, { name: 'Existing', parentId: null });
const folder = await createFolder(portId, TEST_USER_ID, { name: 'Mine', parentId: null });
await expect(renameFolder(portId, folder.id, 'Existing', TEST_USER_ID)).rejects.toThrow(/already exists/i);
await expect(renameFolder(portId, folder.id, 'Existing', TEST_USER_ID)).rejects.toThrow(
/already exists/i,
);
});
it('throws NotFound when the folder belongs to another port', async () => {
const otherPort = await makePort();
const folder = await createFolder(otherPort.id, TEST_USER_ID, { name: 'X', parentId: null });
await expect(renameFolder(portId, folder.id, 'Y', TEST_USER_ID)).rejects.toThrow(/couldn't find/i);
await expect(renameFolder(portId, folder.id, 'Y', TEST_USER_ID)).rejects.toThrow(
/couldn't find/i,
);
});
});

View File

@@ -4,10 +4,7 @@ import { eq } from 'drizzle-orm';
import { db } from '@/lib/db';
import { documentFolders, documents } from '@/lib/db/schema/documents';
import { user } from '@/lib/db/schema/users';
import {
createFolder,
deleteFolderSoftRescue,
} from '@/lib/services/document-folders.service';
import { createFolder, deleteFolderSoftRescue } from '@/lib/services/document-folders.service';
import { makePort } from '../helpers/factories';
describe('document-folders · deleteFolderSoftRescue', () => {
@@ -25,7 +22,7 @@ describe('document-folders · deleteFolderSoftRescue', () => {
await db.delete(documentFolders).where(eq(documentFolders.portId, portId));
});
it('moves child subfolders up to the deleted folder\'s parent', async () => {
it("moves child subfolders up to the deleted folder's parent", async () => {
const root = await createFolder(portId, testUserId, { name: 'Root', parentId: null });
const middle = await createFolder(portId, testUserId, { name: 'Middle', parentId: root.id });
const leaf = await createFolder(portId, testUserId, { name: 'Leaf', parentId: middle.id });
@@ -38,7 +35,7 @@ describe('document-folders · deleteFolderSoftRescue', () => {
expect(survivor?.parentId).toBe(root.id);
});
it('moves child documents to the deleted folder\'s parent', async () => {
it("moves child documents to the deleted folder's parent", async () => {
const root = await createFolder(portId, testUserId, { name: 'Root', parentId: null });
const child = await createFolder(portId, testUserId, { name: 'Child', parentId: root.id });

View File

@@ -98,9 +98,8 @@ describe('GET /api/v1/documents/[id]/download/[...slug]', () => {
storagePath: 'test/contract.pdf',
});
const { downloadHandler } = await import(
'@/app/api/v1/documents/[id]/download/[...slug]/handlers'
);
const { downloadHandler } =
await import('@/app/api/v1/documents/[id]/download/[...slug]/handlers');
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
const req = new Request('http://localhost/api/v1/documents/x/download/whatever') as never;
const res = await downloadHandler(req, ctx, {
@@ -124,9 +123,8 @@ describe('GET /api/v1/documents/[id]/download/[...slug]', () => {
storagePath: 'test/spec.pdf',
});
const { downloadHandler } = await import(
'@/app/api/v1/documents/[id]/download/[...slug]/handlers'
);
const { downloadHandler } =
await import('@/app/api/v1/documents/[id]/download/[...slug]/handlers');
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
const req = new Request('http://localhost/x') as never;
const res = await downloadHandler(req, ctx, {
@@ -147,9 +145,8 @@ describe('GET /api/v1/documents/[id]/download/[...slug]', () => {
storagePath: 'test/real.pdf',
});
const { downloadHandler } = await import(
'@/app/api/v1/documents/[id]/download/[...slug]/handlers'
);
const { downloadHandler } =
await import('@/app/api/v1/documents/[id]/download/[...slug]/handlers');
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
const req = new Request('http://localhost/x') as never;
const res = await downloadHandler(req, ctx, {
@@ -172,9 +169,8 @@ describe('GET /api/v1/documents/[id]/download/[...slug]', () => {
})
.returning();
const { downloadHandler } = await import(
'@/app/api/v1/documents/[id]/download/[...slug]/handlers'
);
const { downloadHandler } =
await import('@/app/api/v1/documents/[id]/download/[...slug]/handlers');
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
const req = new Request('http://localhost/x') as never;
const res = await downloadHandler(req, ctx, {
@@ -199,9 +195,8 @@ describe('GET /api/v1/documents/[id]/download/[...slug]', () => {
storagePath: 'test/a.pdf',
});
const { downloadHandler } = await import(
'@/app/api/v1/documents/[id]/download/[...slug]/handlers'
);
const { downloadHandler } =
await import('@/app/api/v1/documents/[id]/download/[...slug]/handlers');
const ctx = makeMockCtx({ portId: portB.id, permissions: makeFullPermissions() });
const req = new Request('http://localhost/x') as never;
const res = await downloadHandler(req, ctx, {

View File

@@ -9,16 +9,14 @@ import {
describe('document-folder validators', () => {
it('accepts a valid create payload', () => {
expect(createFolderSchema.safeParse({ name: 'Deals', parentId: null }).success).toBe(true);
expect(
createFolderSchema.safeParse({ name: 'Q1', parentId: 'abc-123' }).success,
).toBe(true);
expect(createFolderSchema.safeParse({ name: 'Q1', parentId: 'abc-123' }).success).toBe(true);
});
it('rejects empty + over-long names', () => {
expect(createFolderSchema.safeParse({ name: '', parentId: null }).success).toBe(false);
expect(
createFolderSchema.safeParse({ name: 'x'.repeat(201), parentId: null }).success,
).toBe(false);
expect(createFolderSchema.safeParse({ name: 'x'.repeat(201), parentId: null }).success).toBe(
false,
);
});
it('rejects whitespace-only names', () => {