kennel 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -13
- package/dist/cli/dispatch.js +16 -3
- package/dist/cli/src/cli/dispatch.js +16 -3
- package/dist/cli/src/cli/usage.js +1 -1
- package/dist/cli/src/core/command.js +1 -0
- package/dist/cli/src/core/init.js +5 -5
- package/dist/cli/src/plugins/external.d.ts +28 -2
- package/dist/cli/src/plugins/external.js +75 -21
- package/dist/cli/src/site/server-plugins.d.ts +2 -7
- package/dist/cli/src/site/viewer.js +1 -0
- package/dist/cli/usage.js +1 -1
- package/dist/core/command.js +1 -0
- package/dist/core/init.js +5 -5
- package/dist/core/runtime.js +357 -3207
- package/dist/package/preview.js +1232 -5113
- package/dist/plugins/external.d.ts +28 -2
- package/dist/plugins/external.js +75 -21
- package/dist/site/server-plugins.d.ts +2 -7
- package/dist/site/server-plugins.js +552 -10447
- package/dist/site/viewer.js +1 -0
- package/kernel/apps/web/package.json +1 -5
- package/kernel/apps/web/scripts/dev.mjs +53 -0
- package/kernel/apps/web/src/app.css +0 -3
- package/kernel/apps/web/src/main.tsx +0 -1
- package/kernel/apps/web/src/plugins.ts +0 -30
- package/kernel/apps/web/tsconfig.json +1 -2
- package/kernel/apps/web/vite.config.ts +31 -13
- package/kernel/packages/api/package.json +0 -4
- package/kernel/packages/api/src/server/folder-metadata-plugin.ts +13 -38
- package/kernel/packages/api/src/server/index.ts +0 -1
- package/kernel/packages/api/src/server/plugins.ts +49 -16
- package/kernel/packages/api/test/core.test.ts +13 -25
- package/kernel/packages/markdown/README.md +1 -1
- package/package.json +1 -1
- package/web/assets/{abnfDiagram-VRR7QNED-Bgfb1nEB.js → abnfDiagram-VRR7QNED-xd_g1uK3.js} +1 -1
- package/web/assets/app-CX20nNIa.js +2 -0
- package/web/assets/{app-options-D-RJiQ-X.js → app-options-CkPJE5ep.js} +1 -1
- package/web/assets/{app-view-header-BLvLq02T.js → app-view-header-CNk5KnA-.js} +1 -1
- package/web/assets/{arc-Dk_KI36B.js → arc-B0u5yBJA.js} +1 -1
- package/web/assets/{architectureDiagram-ZJ3FMSHR-NR_KhRCf.js → architectureDiagram-ZJ3FMSHR-DfxOAo8P.js} +1 -1
- package/web/assets/{ask-tray-TzeTcO6-.js → ask-tray-SscJecDY.js} +1 -1
- package/web/assets/{blockDiagram-677ZJIJ3-CkLkwx9C.js → blockDiagram-677ZJIJ3-Ci3Usxh6.js} +1 -1
- package/web/assets/{c4Diagram-LMCZKHZV-CFCUH8e_.js → c4Diagram-LMCZKHZV-BqgwIVgh.js} +1 -1
- package/web/assets/channel-X7WHyxxT.js +1 -0
- package/web/assets/{chunk-2Q5K7J3B-B_BPLWsW.js → chunk-2Q5K7J3B-CfX69l2U.js} +1 -1
- package/web/assets/{chunk-32BRIVSS-B1VB1hOy.js → chunk-32BRIVSS-CuFlzZkl.js} +1 -1
- package/web/assets/{chunk-5VM5RSS4-KFcYr8Co.js → chunk-5VM5RSS4-DidCMlJa.js} +1 -1
- package/web/assets/{chunk-EX3LRPZG-CpBX3kuK.js → chunk-EX3LRPZG-B5CDm1dS.js} +1 -1
- package/web/assets/{chunk-JWPE2WC7-BPSl40wG.js → chunk-JWPE2WC7-D5fGV_sH.js} +1 -1
- package/web/assets/{chunk-MOJQB5TN-D2RcYmoU.js → chunk-MOJQB5TN-ayhH5Q-H.js} +1 -1
- package/web/assets/{chunk-RYQCIY6F-m7Gyn-t3.js → chunk-RYQCIY6F-C4BfOOZn.js} +1 -1
- package/web/assets/{chunk-V7JOEXUC-TDvdopxS.js → chunk-V7JOEXUC-DpUEIM9V.js} +1 -1
- package/web/assets/{chunk-VR4S4FIN-DZTsG51a.js → chunk-VR4S4FIN-DoQuFFRh.js} +1 -1
- package/web/assets/{chunk-XXDRQBXY-Dp0--RE2.js → chunk-XXDRQBXY-CI9eiLwn.js} +1 -1
- package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +1 -0
- package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +1 -0
- package/web/assets/client-7OWw5kW2.js +6 -0
- package/web/assets/client-DYj_td4Q.js +17 -0
- package/web/assets/{client-j2kQmxsO.js → client-Dk-NBvJR.js} +4 -4
- package/web/assets/command-zkzH4hVz.js +38 -0
- package/web/assets/{copy-context-C0Uqsp-3.js → copy-context-D5oiWrYC.js} +1 -1
- package/web/assets/{cose-bilkent-JH36ORCC-CPu3PI8h.js → cose-bilkent-JH36ORCC-BHRk1mO3.js} +1 -1
- package/web/assets/{cynefin-VYW2F7L2-DWxftYAm.js → cynefin-VYW2F7L2-C1B09TRE.js} +1 -1
- package/web/assets/{cynefinDiagram-TSTJHNR4-DsmN2V-p.js → cynefinDiagram-TSTJHNR4-B27qXQbv.js} +1 -1
- package/web/assets/{dagre-VKFMJZFB-COEbApyY.js → dagre-VKFMJZFB-B1qEi32g.js} +1 -1
- package/web/assets/{diagram-FQU43EPY-0O9WGWKB.js → diagram-FQU43EPY-BMkkgADD.js} +1 -1
- package/web/assets/{diagram-G47NLZAW-CutEdMVh.js → diagram-G47NLZAW-Cb8_g1A5.js} +1 -1
- package/web/assets/{diagram-NH7WQ7WH-C9KZc3B2.js → diagram-NH7WQ7WH-B51wuc5m.js} +1 -1
- package/web/assets/{diagram-OA4YK3LP-D8kkPXbY.js → diagram-OA4YK3LP-xnsecbbI.js} +1 -1
- package/web/assets/{diagram-WEI45ONY-_YqiTaGP.js → diagram-WEI45ONY-DSZdyaAV.js} +1 -1
- package/web/assets/{document-Dgt5eZFj.js → document-Dc2dftTW.js} +3 -3
- package/web/assets/{ebnfDiagram-CCIWWBDH-JO-zQh6g.js → ebnfDiagram-CCIWWBDH-aTHmcUGy.js} +1 -1
- package/web/assets/{entities-C3wz7FCx.js → entities-CCUtpskY.js} +1 -1
- package/web/assets/{erDiagram-Q63AITRT-9UjQwJ4P.js → erDiagram-Q63AITRT-CUUPBcvo.js} +1 -1
- package/web/assets/{file-viewer-DZJ8nYnl.js → file-viewer-DJ3rsmef.js} +1 -1
- package/web/assets/{flowDiagram-23GEKE2U-Ca08tnYM.js → flowDiagram-23GEKE2U-aTQXx2Mh.js} +1 -1
- package/web/assets/{folder-image-BfGgxo7i.js → folder-image-Bnv43tzP.js} +1 -1
- package/web/assets/{folder-landing-qUKBCnG-.js → folder-landing-DCnzYXXY.js} +1 -1
- package/web/assets/{folder-list-BfFjv7rR.js → folder-list-DvVkVjiR.js} +1 -1
- package/web/assets/{folder-metadata-DCw8xzHk.js → folder-metadata-C9hMMzIH.js} +1 -1
- package/web/assets/{folder-mobile-bar-Ubz4babf.js → folder-mobile-bar-VoTL1GS0.js} +1 -1
- package/web/assets/folder-plugin-view-Cvd804ZU.js +6 -0
- package/web/assets/{ganttDiagram-NO4QXBWP-KnHzX5W3.js → ganttDiagram-NO4QXBWP-DiwdUp7u.js} +1 -1
- package/web/assets/{gitGraphDiagram-IHSO6WYX-Cm8woNtK.js → gitGraphDiagram-IHSO6WYX-BL2gG_IS.js} +1 -1
- package/web/assets/{index-CNyy98F8.js → index-CUTtMSyJ.js} +2 -2
- package/web/assets/index-CegvVQ1C.js +1 -0
- package/web/assets/index-Dwhf8F_U.css +1 -0
- package/web/assets/{infoDiagram-FWYZ7A6U-yyqmUtIb.js → infoDiagram-FWYZ7A6U-B3n0zZpt.js} +1 -1
- package/web/assets/{ishikawaDiagram-FXEZZL3T-BfvR_Zy4.js → ishikawaDiagram-FXEZZL3T-D7YytbEv.js} +1 -1
- package/web/assets/{journeyDiagram-5HDEW3XC-BKuw06HM.js → journeyDiagram-5HDEW3XC-BYwqEl3C.js} +1 -1
- package/web/assets/{kanban-definition-HUTT4EX6-n0010rmQ.js → kanban-definition-HUTT4EX6-BQHNJgXZ.js} +1 -1
- package/web/assets/{linear-C0d0lT8O.js → linear-BPiQCLNk.js} +1 -1
- package/web/assets/{mermaid.core-BDVbShyO.js → mermaid.core-CD-KvBjV.js} +4 -4
- package/web/assets/{metadata-KV0dpp4G.js → metadata-COJ7fZul.js} +1 -1
- package/web/assets/{mindmap-definition-LN4V7U3C--_0ks3GF.js → mindmap-definition-LN4V7U3C-C1L_gyoj.js} +1 -1
- package/web/assets/{pegDiagram-2B236MQR-CBoNIpKR.js → pegDiagram-2B236MQR-BDAfItqS.js} +1 -1
- package/web/assets/{pieDiagram-ENE6RG2P-MA5tkYh5.js → pieDiagram-ENE6RG2P-CkZYH3Vz.js} +1 -1
- package/web/assets/{popover-DBD7FF-W.js → popover-0oA2lLUV.js} +2 -2
- package/web/assets/{project-search-dialog-DM1IkSZu.js → project-search-dialog-B2oqMT6h.js} +1 -1
- package/web/assets/{quadrantDiagram-ABIIQ3AL-CWT1ryqF.js → quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js} +1 -1
- package/web/assets/{railroadDiagram-RFXS5EU6-ZiLB5FGi.js → railroadDiagram-RFXS5EU6-Dd9_yo7B.js} +1 -1
- package/web/assets/{requirementDiagram-TGXJPOKE-CQq9uneq.js → requirementDiagram-TGXJPOKE-DPhIUsbs.js} +1 -1
- package/web/assets/{sankeyDiagram-HTMAVEWB-qDG-0JqI.js → sankeyDiagram-HTMAVEWB-B2QkYtub.js} +1 -1
- package/web/assets/{search-DNrQA5fF.js → search-C1J0-xiB.js} +1 -1
- package/web/assets/select-CtXC_PLC.js +30 -0
- package/web/assets/{selection-popup-DmtTeRs2.js → selection-popup-qhlUFzGq.js} +1 -1
- package/web/assets/{sequenceDiagram-DBY2YBRQ-CiApurLO.js → sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js} +1 -1
- package/web/assets/{sizeCapture-X5ZJPWSS-DZSR5QXm.js → sizeCapture-X5ZJPWSS-CIXIhl_A.js} +1 -1
- package/web/assets/{stateDiagram-2N3HPSRC-CjJJR9_z.js → stateDiagram-2N3HPSRC-DCrdAflB.js} +1 -1
- package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +1 -0
- package/web/assets/{swimlanes-5IMT3BWC-CKmo1sg6.js → swimlanes-5IMT3BWC-DA2vc7iZ.js} +2 -2
- package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +8 -0
- package/web/assets/{timeline-definition-FHXFAJF6-D-NMwS3y.js → timeline-definition-FHXFAJF6-lKfJiV9V.js} +1 -1
- package/web/assets/{vennDiagram-L72KCM5P-CfL6LBWw.js → vennDiagram-L72KCM5P-DO1q2KzG.js} +1 -1
- package/web/assets/{view-context-gyDl_p2V.js → view-context-DXKM3-l1.js} +4 -4
- package/web/assets/{viewer-capabilities-Bpp8Uvcg.js → viewer-capabilities-Cig_xM9u.js} +1 -1
- package/web/assets/{wardleyDiagram-EHGQE667-DgldMxaj.js → wardleyDiagram-EHGQE667-Dal9LkuZ.js} +1 -1
- package/web/assets/{xychartDiagram-FW5EYKEG-CIoAIFX8.js → xychartDiagram-FW5EYKEG-B3dPLPII.js} +1 -1
- package/web/index.html +2 -2
- package/kernel/packages/api/test/partial-loading.test.ts +0 -43
- package/kernel/plugins/calendar/SKILL.md +0 -173
- package/kernel/plugins/calendar/eslint.config.js +0 -10
- package/kernel/plugins/calendar/package.json +0 -73
- package/kernel/plugins/calendar/schema.json +0 -137
- package/kernel/plugins/calendar/src/calendar.tsx +0 -328
- package/kernel/plugins/calendar/src/client.tsx +0 -139
- package/kernel/plugins/calendar/src/context.ts +0 -48
- package/kernel/plugins/calendar/src/copy-button.tsx +0 -66
- package/kernel/plugins/calendar/src/event-dialog.tsx +0 -270
- package/kernel/plugins/calendar/src/index.ts +0 -4
- package/kernel/plugins/calendar/src/manifest.ts +0 -7
- package/kernel/plugins/calendar/src/parse.ts +0 -610
- package/kernel/plugins/calendar/src/server.ts +0 -160
- package/kernel/plugins/calendar/src/utils.ts +0 -80
- package/kernel/plugins/calendar/src/views.tsx +0 -418
- package/kernel/plugins/calendar/test/context.test.mjs +0 -60
- package/kernel/plugins/calendar/test/package.test.mjs +0 -43
- package/kernel/plugins/calendar/test/parse.test.mjs +0 -392
- package/kernel/plugins/calendar/test/server.test.mjs +0 -126
- package/kernel/plugins/calendar/tsconfig.json +0 -17
- package/kernel/plugins/mail/SKILL.md +0 -97
- package/kernel/plugins/mail/eslint.config.js +0 -10
- package/kernel/plugins/mail/package.json +0 -79
- package/kernel/plugins/mail/src/api.ts +0 -359
- package/kernel/plugins/mail/src/bun-ffi.d.ts +0 -14
- package/kernel/plugins/mail/src/client.tsx +0 -14
- package/kernel/plugins/mail/src/flock.ts +0 -137
- package/kernel/plugins/mail/src/folders.ts +0 -37
- package/kernel/plugins/mail/src/history.ts +0 -485
- package/kernel/plugins/mail/src/index.ts +0 -3
- package/kernel/plugins/mail/src/mail-card.tsx +0 -123
- package/kernel/plugins/mail/src/mail-conversation.tsx +0 -153
- package/kernel/plugins/mail/src/mail-detail-page.tsx +0 -467
- package/kernel/plugins/mail/src/mail-hooks.ts +0 -252
- package/kernel/plugins/mail/src/mail-page.tsx +0 -109
- package/kernel/plugins/mail/src/mail-request.ts +0 -78
- package/kernel/plugins/mail/src/mail-send-box.tsx +0 -175
- package/kernel/plugins/mail/src/mailos.ts +0 -723
- package/kernel/plugins/mail/src/manifest.ts +0 -7
- package/kernel/plugins/mail/src/node.ts +0 -171
- package/kernel/plugins/mail/src/parse.ts +0 -139
- package/kernel/plugins/mail/src/server.ts +0 -125
- package/kernel/plugins/mail/src/types.ts +0 -10
- package/kernel/plugins/mail/src/viewer.tsx +0 -147
- package/kernel/plugins/mail/test/flock.test.mjs +0 -156
- package/kernel/plugins/mail/test/folders.test.mjs +0 -138
- package/kernel/plugins/mail/test/history.test.mjs +0 -460
- package/kernel/plugins/mail/test/mailos.test.mjs +0 -752
- package/kernel/plugins/mail/test/node.test.mjs +0 -207
- package/kernel/plugins/mail/test/package.test.mjs +0 -54
- package/kernel/plugins/mail/test/parse.test.mjs +0 -32
- package/kernel/plugins/mail/test/server.test.mjs +0 -116
- package/kernel/plugins/mail/test/viewer.test.mjs +0 -192
- package/kernel/plugins/mail/tsconfig.json +0 -17
- package/kernel/plugins/projects/SKILL.md +0 -73
- package/kernel/plugins/projects/eslint.config.js +0 -10
- package/kernel/plugins/projects/package.json +0 -70
- package/kernel/plugins/projects/src/client.tsx +0 -17
- package/kernel/plugins/projects/src/data/model.ts +0 -129
- package/kernel/plugins/projects/src/index.ts +0 -17
- package/kernel/plugins/projects/src/manifest.ts +0 -7
- package/kernel/plugins/projects/src/project-context.ts +0 -41
- package/kernel/plugins/projects/src/server.ts +0 -276
- package/kernel/plugins/projects/src/ui/project-card.tsx +0 -61
- package/kernel/plugins/projects/src/ui/projects-page.tsx +0 -102
- package/kernel/plugins/projects/test/package.test.mjs +0 -51
- package/kernel/plugins/projects/tsconfig.json +0 -17
- package/kernel/plugins/tasks/SKILL.md +0 -341
- package/kernel/plugins/tasks/eslint.config.js +0 -10
- package/kernel/plugins/tasks/package.json +0 -74
- package/kernel/plugins/tasks/schema.json +0 -939
- package/kernel/plugins/tasks/src/api.ts +0 -212
- package/kernel/plugins/tasks/src/client.tsx +0 -23
- package/kernel/plugins/tasks/src/data/parse.ts +0 -544
- package/kernel/plugins/tasks/src/data/task-id.ts +0 -56
- package/kernel/plugins/tasks/src/data/todo-model.ts +0 -795
- package/kernel/plugins/tasks/src/index.ts +0 -34
- package/kernel/plugins/tasks/src/manifest.ts +0 -7
- package/kernel/plugins/tasks/src/pages/task-detail.tsx +0 -48
- package/kernel/plugins/tasks/src/pages/task-list.tsx +0 -88
- package/kernel/plugins/tasks/src/server.ts +0 -447
- package/kernel/plugins/tasks/src/styles.css +0 -9
- package/kernel/plugins/tasks/src/ui/compact-todo-list.tsx +0 -372
- package/kernel/plugins/tasks/src/ui/error-details.tsx +0 -78
- package/kernel/plugins/tasks/src/ui/task-page.tsx +0 -392
- package/kernel/plugins/tasks/src/ui/todo-agent-filter.tsx +0 -36
- package/kernel/plugins/tasks/src/ui/todo-filter-bar.tsx +0 -66
- package/kernel/plugins/tasks/src/ui/todo-item.tsx +0 -494
- package/kernel/plugins/tasks/src/ui/todo-status-filter.tsx +0 -60
- package/kernel/plugins/tasks/src/ui/todo-tag-filter.tsx +0 -48
- package/kernel/plugins/tasks/src/ui/todo-warnings.tsx +0 -106
- package/kernel/plugins/tasks/test/api.test.mjs +0 -99
- package/kernel/plugins/tasks/test/folder-sources.test.mjs +0 -46
- package/kernel/plugins/tasks/test/package.test.mjs +0 -54
- package/kernel/plugins/tasks/test/parse.test.mjs +0 -225
- package/kernel/plugins/tasks/test/server.test.mjs +0 -386
- package/kernel/plugins/tasks/tsconfig.json +0 -17
- package/web/assets/app-DoGr2Zi7.js +0 -2
- package/web/assets/channel-uMEjV8qc.js +0 -1
- package/web/assets/classDiagram-OUVF2IWQ-fbZocVor.js +0 -1
- package/web/assets/classDiagram-v2-EOCWNBFH-fbZocVor.js +0 -1
- package/web/assets/client-C6Nxdfif.js +0 -2
- package/web/assets/client-CXYfBko5.js +0 -17
- package/web/assets/client-CoKtOq-X.js +0 -7
- package/web/assets/client-DyRQz5RU.js +0 -53
- package/web/assets/client-OY6HUzuD.js +0 -98
- package/web/assets/client-YOYWZIFi.js +0 -34
- package/web/assets/command-BugZBGXE.js +0 -38
- package/web/assets/folder-plugin-view-DsWR1zmQ.js +0 -6
- package/web/assets/index-BaRnbYRw.js +0 -1
- package/web/assets/index-BexmLxIP.css +0 -1
- package/web/assets/index-DDirZMgx.js +0 -1
- package/web/assets/note-card-DBfX6gW1.js +0 -6
- package/web/assets/select-D8cqNgcp.js +0 -30
- package/web/assets/stateDiagram-v2-6OUMAXLB-BpeGSCu9.js +0 -1
- package/web/assets/swimlanesDiagram-G3AALYLV-D9RjLKb9.js +0 -8
- package/web/assets/use-folder-filter-LQyXns5Q.js +0 -1
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"
|
|
2
|
-
import { tmpdir } from "node:os"
|
|
3
|
-
import { join, resolve } from "node:path"
|
|
4
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
|
|
5
|
-
|
|
6
|
-
import { isFolderWithin, listMailForFolder, readMailById } from "../src/node.ts"
|
|
7
|
-
|
|
8
|
-
let tmpBase
|
|
9
|
-
let root
|
|
10
|
-
let sibling
|
|
11
|
-
let outside
|
|
12
|
-
|
|
13
|
-
beforeAll(async () => {
|
|
14
|
-
tmpBase = await mkdtemp(join(tmpdir(), "kennel-mail-nested-"))
|
|
15
|
-
root = join(tmpBase, "project")
|
|
16
|
-
sibling = join(tmpBase, "project-x")
|
|
17
|
-
outside = join(tmpBase, "outside")
|
|
18
|
-
await mkdir(join(root, "clients", "client-one"), { recursive: true })
|
|
19
|
-
await mkdir(join(root, "client-one"), { recursive: true })
|
|
20
|
-
await mkdir(sibling, { recursive: true })
|
|
21
|
-
await mkdir(outside, { recursive: true })
|
|
22
|
-
// Symlink escape: a "folder" inside root that realpaths outside it.
|
|
23
|
-
await mkdir(join(root, "clients"), { recursive: true })
|
|
24
|
-
await symlink(outside, join(root, "clients", "escape"))
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
afterAll(async () => {
|
|
28
|
-
await rm(tmpBase, { recursive: true, force: true })
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
describe("isFolderWithin", () => {
|
|
32
|
-
test("exact folder is within itself", () => {
|
|
33
|
-
expect(isFolderWithin(root, root)).toBe(true)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test("real descendant is within", () => {
|
|
37
|
-
expect(isFolderWithin(root, join(root, "clients", "client-one"))).toBe(true)
|
|
38
|
-
expect(isFolderWithin(root, join(root, "clients"))).toBe(true)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
test("sibling prefix path is rejected", () => {
|
|
42
|
-
expect(isFolderWithin(root, sibling)).toBe(false)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
test("outside ancestor is rejected", () => {
|
|
46
|
-
expect(isFolderWithin(root, tmpBase)).toBe(false)
|
|
47
|
-
expect(isFolderWithin(root, tmpdir())).toBe(false)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test("traversal escapes collapse and are resolved against real paths", () => {
|
|
51
|
-
expect(isFolderWithin(root, join(root, "clients", "..", ".."))).toBe(false)
|
|
52
|
-
expect(
|
|
53
|
-
isFolderWithin(root, join(root, "clients", "..", "client-one")),
|
|
54
|
-
).toBe(true)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test("symlink escape is rejected", () => {
|
|
58
|
-
expect(isFolderWithin(root, join(root, "clients", "escape"))).toBe(false)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
test("unresolved (missing) candidate is rejected", () => {
|
|
62
|
-
expect(isFolderWithin(root, join(root, "not-created-yet"))).toBe(false)
|
|
63
|
-
expect(isFolderWithin(join(root, "not-created-yet"), root)).toBe(false)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test("null or undefined candidate is rejected", () => {
|
|
67
|
-
expect(isFolderWithin(root, null)).toBe(false)
|
|
68
|
-
expect(isFolderWithin(root, undefined)).toBe(false)
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
function draftFile(folder, id, subject = `Subject ${id}`) {
|
|
73
|
-
const folderLine = folder ? `folder: ${folder}` : ""
|
|
74
|
-
return `---\nid: ${id}\n${folderLine}\nstatus: working\nsubject: ${subject}\n---\n\nBody of ${id}\n`
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
describe("listMailForFolder", () => {
|
|
78
|
-
test("lists exact and descendant folders only", async () => {
|
|
79
|
-
const mailDir = join(tmpBase, "drafts")
|
|
80
|
-
await mkdir(mailDir, { recursive: true })
|
|
81
|
-
await writeFile(
|
|
82
|
-
join(mailDir, "d_root.md"),
|
|
83
|
-
draftFile(resolve(root), "d_root"),
|
|
84
|
-
)
|
|
85
|
-
await writeFile(
|
|
86
|
-
join(mailDir, "d_nested.md"),
|
|
87
|
-
draftFile(resolve(root, "clients", "client-one"), "d_nested"),
|
|
88
|
-
)
|
|
89
|
-
await writeFile(
|
|
90
|
-
join(mailDir, "d_nested2.md"),
|
|
91
|
-
draftFile(resolve(root, "clients"), "d_nested2"),
|
|
92
|
-
)
|
|
93
|
-
await writeFile(
|
|
94
|
-
join(mailDir, "d_sibling.md"),
|
|
95
|
-
draftFile(resolve(sibling), "d_sibling"),
|
|
96
|
-
)
|
|
97
|
-
await writeFile(
|
|
98
|
-
join(mailDir, "d_outside.md"),
|
|
99
|
-
draftFile(resolve(outside), "d_outside"),
|
|
100
|
-
)
|
|
101
|
-
await writeFile(
|
|
102
|
-
join(mailDir, "d_escape.md"),
|
|
103
|
-
draftFile(resolve(root, "clients", "escape"), "d_escape"),
|
|
104
|
-
)
|
|
105
|
-
await writeFile(
|
|
106
|
-
join(mailDir, "d_nofolder.md"),
|
|
107
|
-
draftFile(null, "d_nofolder"),
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
const items = await listMailForFolder({ mailDir, folder: root })
|
|
111
|
-
const ids = items.map((item) => item.id).sort()
|
|
112
|
-
expect(ids).toEqual(["d_nested", "d_nested2", "d_root"])
|
|
113
|
-
|
|
114
|
-
const nested = items.find((item) => item.id === "d_nested")
|
|
115
|
-
expect(nested?.folder).toBe(resolve(root, "clients", "client-one"))
|
|
116
|
-
expect(nested?.subject).toBe("Subject d_nested")
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
test("nested folder scope works for a subfolder row", async () => {
|
|
120
|
-
const mailDir = join(tmpBase, "drafts")
|
|
121
|
-
const items = await listMailForFolder({
|
|
122
|
-
mailDir,
|
|
123
|
-
folder: resolve(root, "clients"),
|
|
124
|
-
})
|
|
125
|
-
const ids = items.map((item) => item.id).sort()
|
|
126
|
-
expect(ids).toContain("d_nested")
|
|
127
|
-
expect(ids).toContain("d_nested2")
|
|
128
|
-
expect(ids).not.toContain("d_root")
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test("reads only working and ready drafts from the mail folder", async () => {
|
|
132
|
-
const mailDir = join(tmpBase, "active-drafts")
|
|
133
|
-
await mkdir(mailDir, { recursive: true })
|
|
134
|
-
|
|
135
|
-
const statusDraft = (id, status) =>
|
|
136
|
-
`---\nid: ${id}\nfolder: ${resolve(root)}\nstatus: ${status}\nsubject: ${id}\n---\n\nBody of ${id}\n`
|
|
137
|
-
await writeFile(
|
|
138
|
-
join(mailDir, "d_aaaaaa.md"),
|
|
139
|
-
statusDraft("d_aaaaaa", "working"),
|
|
140
|
-
)
|
|
141
|
-
await writeFile(
|
|
142
|
-
join(mailDir, "d_ccccc1.md"),
|
|
143
|
-
statusDraft("d_ccccc1", "ready"),
|
|
144
|
-
)
|
|
145
|
-
await writeFile(
|
|
146
|
-
join(mailDir, "d_bbbbbb.md"),
|
|
147
|
-
statusDraft("d_bbbbbb", "sent"),
|
|
148
|
-
)
|
|
149
|
-
await writeFile(
|
|
150
|
-
join(mailDir, "d_dddddd.md"),
|
|
151
|
-
statusDraft("d_dddddd", "archived"),
|
|
152
|
-
)
|
|
153
|
-
await writeFile(join(mailDir, "d_eeeeee.md"), `# No status\n`)
|
|
154
|
-
|
|
155
|
-
const items = await listMailForFolder({ mailDir, folder: root })
|
|
156
|
-
const ids = items.map((item) => item.id).sort()
|
|
157
|
-
expect(ids).toEqual(["d_aaaaaa", "d_ccccc1"])
|
|
158
|
-
})
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
describe("readMailById", () => {
|
|
162
|
-
test("reads the authoritative draft file by id", async () => {
|
|
163
|
-
const mailDir = join(tmpBase, "direct-path")
|
|
164
|
-
await mkdir(mailDir, { recursive: true })
|
|
165
|
-
|
|
166
|
-
const path = join(mailDir, "d_cccccc.md")
|
|
167
|
-
await writeFile(path, draftFile(resolve(root), "d_cccccc"))
|
|
168
|
-
|
|
169
|
-
const mail = await readMailById("d_cccccc", mailDir)
|
|
170
|
-
|
|
171
|
-
expect(mail?.id).toBe("d_cccccc")
|
|
172
|
-
expect(mail?.filePath).toBe(path)
|
|
173
|
-
expect(mail?.body).toBe(`Body of d_cccccc\n`)
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
test("supports MailOS descriptive draft ids", async () => {
|
|
177
|
-
const mailDir = join(tmpBase, "descriptive-ids")
|
|
178
|
-
await mkdir(mailDir, { recursive: true })
|
|
179
|
-
|
|
180
|
-
const id = "draft_20251129_215834_Test-Draft"
|
|
181
|
-
await writeFile(join(mailDir, `${id}.md`), draftFile(resolve(root), id))
|
|
182
|
-
|
|
183
|
-
const mail = await readMailById(id, mailDir)
|
|
184
|
-
expect(mail?.id).toBe(id)
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
test("rejects drafts that are no longer active", async () => {
|
|
188
|
-
const mailDir = join(tmpBase, "inactive-drafts")
|
|
189
|
-
await mkdir(mailDir, { recursive: true })
|
|
190
|
-
|
|
191
|
-
await writeFile(
|
|
192
|
-
join(mailDir, "d_bbbbbb.md"),
|
|
193
|
-
`---\nid: d_bbbbbb\nfolder: ${resolve(root)}\nstatus: sent\nsubject: Sent\n---\n\nBody\n`,
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
const mail = await readMailById("d_bbbbbb", mailDir)
|
|
197
|
-
expect(mail).toBeNull()
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
test("rejects traversal ids", () => {
|
|
201
|
-
const mailDir = join(tmpBase, "traversal-drafts")
|
|
202
|
-
expect(() => readMailById("../d_aaaaaa", mailDir)).toThrow(
|
|
203
|
-
"Invalid mail id",
|
|
204
|
-
)
|
|
205
|
-
expect(() => readMailById("a/b", mailDir)).toThrow("Invalid mail id")
|
|
206
|
-
})
|
|
207
|
-
})
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict"
|
|
2
|
-
import { readFile } from "node:fs/promises"
|
|
3
|
-
import test from "node:test"
|
|
4
|
-
|
|
5
|
-
const packageJson = JSON.parse(
|
|
6
|
-
await readFile(new URL("../package.json", import.meta.url), "utf8"),
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
test("mail package is ready for public npm publishing", () => {
|
|
10
|
-
assert.equal(packageJson.private, undefined)
|
|
11
|
-
assert.equal(packageJson.version, "0.1.0")
|
|
12
|
-
assert.equal(packageJson.license, "MIT")
|
|
13
|
-
assert.equal(packageJson.publishConfig?.access, "public")
|
|
14
|
-
assert.equal(packageJson.repository?.directory, "plugins/mail")
|
|
15
|
-
assert.deepEqual(packageJson.files, ["src"])
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
test("mail package ships the standard plugin script set", () => {
|
|
19
|
-
for (const script of [
|
|
20
|
-
"test",
|
|
21
|
-
"typecheck",
|
|
22
|
-
"format",
|
|
23
|
-
"lint",
|
|
24
|
-
"clean",
|
|
25
|
-
"pack:check",
|
|
26
|
-
"publish:public:dry-run",
|
|
27
|
-
]) {
|
|
28
|
-
assert.ok(
|
|
29
|
-
typeof packageJson.scripts?.[script] === "string",
|
|
30
|
-
`missing script "${script}"`,
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
test("mail package exposes the core plugin deep exports", () => {
|
|
36
|
-
const keys = Object.keys(packageJson.exports).sort()
|
|
37
|
-
for (const required of [
|
|
38
|
-
".",
|
|
39
|
-
"./client",
|
|
40
|
-
"./manifest",
|
|
41
|
-
"./parse",
|
|
42
|
-
"./server",
|
|
43
|
-
]) {
|
|
44
|
-
assert.ok(keys.includes(required), `missing export "${required}"`)
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test("mail client entry does not import the Node server entry", async () => {
|
|
49
|
-
const clientSource = await readFile(
|
|
50
|
-
new URL("../src/client.tsx", import.meta.url),
|
|
51
|
-
"utf8",
|
|
52
|
-
)
|
|
53
|
-
assert.doesNotMatch(clientSource, /node:|\/server"/)
|
|
54
|
-
})
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
|
|
3
|
-
import { parseMailMarkdown } from "../src/parse.ts"
|
|
4
|
-
|
|
5
|
-
function baseMail(body = "Hello") {
|
|
6
|
-
return `---\nid: d_abcdef\nfolder: /tmp/project\n---\n\n${body}\n`
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
describe("parseMailMarkdown", () => {
|
|
10
|
-
test("parses normal frontmatter", () => {
|
|
11
|
-
const parsed = parseMailMarkdown(baseMail("Body"))
|
|
12
|
-
expect(parsed.id).toBe("d_abcdef")
|
|
13
|
-
expect(parsed.folder).toBe("/tmp/project")
|
|
14
|
-
expect(parsed.body).toBe("Body\n")
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test("empty frontmatter does not throw and returns defaults", () => {
|
|
18
|
-
const content = `---\n---\n\nBody\n`
|
|
19
|
-
const parsed = parseMailMarkdown(content)
|
|
20
|
-
expect(parsed.id).toBe(null)
|
|
21
|
-
expect(parsed.folder).toBe(null)
|
|
22
|
-
expect(parsed.body).toBe("Body\n")
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test("non-object frontmatter does not throw and returns defaults", () => {
|
|
26
|
-
const content = `---\n[]\n---\n\nBody\n`
|
|
27
|
-
const parsed = parseMailMarkdown(content)
|
|
28
|
-
expect(parsed.id).toBe(null)
|
|
29
|
-
expect(parsed.folder).toBe(null)
|
|
30
|
-
expect(parsed.body).toBe("Body\n")
|
|
31
|
-
})
|
|
32
|
-
})
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
2
|
-
import { tmpdir } from "node:os"
|
|
3
|
-
import { join } from "node:path"
|
|
4
|
-
import { afterAll, beforeAll, expect, test } from "bun:test"
|
|
5
|
-
|
|
6
|
-
import { createMailPlugin } from "../src/server.ts"
|
|
7
|
-
|
|
8
|
-
function draftFile(folder, id) {
|
|
9
|
-
return `---\nid: ${id}\nfolder: ${folder}\nstatus: working\nsubject: ${id}\n---\n\nBody of ${id}\n`
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
let base
|
|
13
|
-
let root
|
|
14
|
-
let mailDir
|
|
15
|
-
|
|
16
|
-
beforeAll(async () => {
|
|
17
|
-
base = await mkdtemp(join(tmpdir(), "kennel-mail-plugin-"))
|
|
18
|
-
root = join(base, "project")
|
|
19
|
-
mailDir = join(base, "drafts")
|
|
20
|
-
await mkdir(join(root, "clients"), { recursive: true })
|
|
21
|
-
await mkdir(mailDir, { recursive: true })
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
afterAll(async () => {
|
|
25
|
-
await rm(base, { recursive: true, force: true })
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
test("detect is unavailable with no active drafts", async () => {
|
|
29
|
-
const plugin = createMailPlugin({ mailDir })
|
|
30
|
-
const detection = await plugin.detect({ folder: root })
|
|
31
|
-
expect(detection.status).toBe("unavailable")
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test("detect reports available with a count of matching active drafts", async () => {
|
|
35
|
-
const nested = join(root, "clients", "nested")
|
|
36
|
-
await mkdir(nested, { recursive: true })
|
|
37
|
-
await writeFile(join(mailDir, "d_root.md"), draftFile(root, "d_root"))
|
|
38
|
-
await writeFile(join(mailDir, "d_nested.md"), draftFile(nested, "d_nested"))
|
|
39
|
-
|
|
40
|
-
const plugin = createMailPlugin({ mailDir })
|
|
41
|
-
const detection = await plugin.detect({ folder: root })
|
|
42
|
-
expect(detection.status).toBe("available")
|
|
43
|
-
expect(detection.count).toBe(2)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test("handle list returns only folder-and-descendant active drafts", async () => {
|
|
47
|
-
const plugin = createMailPlugin({ mailDir })
|
|
48
|
-
const result = await plugin.handle({ folder: root }, "")
|
|
49
|
-
expect(result.kind).toBe("available")
|
|
50
|
-
if (result.kind !== "available") return
|
|
51
|
-
const payload = result.data
|
|
52
|
-
expect(payload.status).toBe("available")
|
|
53
|
-
const ids = payload.items.map((item) => item.id).sort()
|
|
54
|
-
expect(ids).toEqual(["d_nested", "d_root"])
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test("handle detail returns the mail and injected preview", async () => {
|
|
58
|
-
const calls = []
|
|
59
|
-
const plugin = createMailPlugin({
|
|
60
|
-
mailDir,
|
|
61
|
-
previewer: async (options) => {
|
|
62
|
-
calls.push(options.id)
|
|
63
|
-
return { body: "COMPOSED BODY", signature: "--\nSig" }
|
|
64
|
-
},
|
|
65
|
-
})
|
|
66
|
-
const result = await plugin.handle({ folder: root }, "d_root")
|
|
67
|
-
expect(result.kind).toBe("available")
|
|
68
|
-
if (result.kind !== "available") return
|
|
69
|
-
expect(result.data.mail.id).toBe("d_root")
|
|
70
|
-
expect(result.data.preview.body).toBe("COMPOSED BODY")
|
|
71
|
-
expect(calls).toEqual(["d_root"])
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test("handle detail rejects a folder mismatch before preview", async () => {
|
|
75
|
-
const other = join(base, "other")
|
|
76
|
-
await mkdir(other, { recursive: true })
|
|
77
|
-
let called = false
|
|
78
|
-
const plugin = createMailPlugin({
|
|
79
|
-
mailDir,
|
|
80
|
-
previewer: async () => {
|
|
81
|
-
called = true
|
|
82
|
-
return { body: "x", signature: null }
|
|
83
|
-
},
|
|
84
|
-
})
|
|
85
|
-
await expect(plugin.handle({ folder: other }, "d_root")).rejects.toThrow(
|
|
86
|
-
"Mail not found for this folder",
|
|
87
|
-
)
|
|
88
|
-
expect(called).toBe(false)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test("handle detail maps a preview read failure to a server failure", async () => {
|
|
92
|
-
const plugin = createMailPlugin({
|
|
93
|
-
mailDir,
|
|
94
|
-
previewer: async () => {
|
|
95
|
-
throw new Error("mailos parse failed")
|
|
96
|
-
},
|
|
97
|
-
})
|
|
98
|
-
await expect(plugin.handle({ folder: root }, "d_root")).rejects.toMatchObject(
|
|
99
|
-
{
|
|
100
|
-
name: "FolderPluginFailure",
|
|
101
|
-
},
|
|
102
|
-
)
|
|
103
|
-
await expect(plugin.handle({ folder: root }, "d_root")).rejects.toThrow(
|
|
104
|
-
"Mail preview failed",
|
|
105
|
-
)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test("handle detail rejects traversal ids", async () => {
|
|
109
|
-
const plugin = createMailPlugin({ mailDir })
|
|
110
|
-
await expect(plugin.handle({ folder: root }, "../x")).rejects.toThrow(
|
|
111
|
-
"Invalid mail id",
|
|
112
|
-
)
|
|
113
|
-
await expect(plugin.handle({ folder: root }, "a/b")).rejects.toThrow(
|
|
114
|
-
"Invalid mail id",
|
|
115
|
-
)
|
|
116
|
-
})
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { createElement as h } from "react"
|
|
2
|
-
import { describe, expect, test } from "bun:test"
|
|
3
|
-
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
-
|
|
5
|
-
import { folderChoices, folderLabel } from "../src/folders.ts"
|
|
6
|
-
import { MailFolderFilter, mailItemContext, MailList } from "../src/viewer.tsx"
|
|
7
|
-
|
|
8
|
-
const WORKSPACE = "/tmp/kennel-example/example-workspace"
|
|
9
|
-
const CLIENT_ONE = `${WORKSPACE}/clients/client-one`
|
|
10
|
-
const CLIENT_TWO = `${WORKSPACE}/clients/client-two`
|
|
11
|
-
const CLIENT_THREE = `${WORKSPACE}/clients/client-three`
|
|
12
|
-
|
|
13
|
-
function mailItem({ folder, id, subject }) {
|
|
14
|
-
return {
|
|
15
|
-
bcc: [],
|
|
16
|
-
cc: [],
|
|
17
|
-
createdAt: "2026-09-18T08:23:23.805127Z",
|
|
18
|
-
excerpt: "Draft excerpt",
|
|
19
|
-
filePath: `/tmp/${id}.md`,
|
|
20
|
-
folder,
|
|
21
|
-
id,
|
|
22
|
-
status: "working",
|
|
23
|
-
subject,
|
|
24
|
-
tags: [],
|
|
25
|
-
to: ["ed@example.com"],
|
|
26
|
-
updatedAt: null,
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function renderMailList(items) {
|
|
31
|
-
return renderToStaticMarkup(
|
|
32
|
-
h(MailList, {
|
|
33
|
-
folderLabel: (folder) => folderLabel(folder, WORKSPACE),
|
|
34
|
-
items,
|
|
35
|
-
mailHref: (id) => `/folder/x/mail/${id}`,
|
|
36
|
-
}),
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("MailList cards", () => {
|
|
41
|
-
test("each card shows its owning folder and keeps a stable link", () => {
|
|
42
|
-
const html = renderMailList([
|
|
43
|
-
mailItem({ folder: WORKSPACE, id: "d_root0", subject: "Root draft" }),
|
|
44
|
-
mailItem({
|
|
45
|
-
folder: CLIENT_ONE,
|
|
46
|
-
id: "d_client-one0",
|
|
47
|
-
subject: "Client One draft",
|
|
48
|
-
}),
|
|
49
|
-
mailItem({ folder: null, id: "d_none0", subject: "No folder draft" }),
|
|
50
|
-
])
|
|
51
|
-
|
|
52
|
-
expect(html).toContain("Root draft")
|
|
53
|
-
expect(html).toContain("Client One draft")
|
|
54
|
-
expect(html).toContain("No folder draft")
|
|
55
|
-
// Stable per-draft links survive filtering.
|
|
56
|
-
expect(html).toContain('href="/folder/x/mail/d_root0"')
|
|
57
|
-
expect(html).toContain('href="/folder/x/mail/d_client-one0"')
|
|
58
|
-
expect(html).toContain('href="/folder/x/mail/d_none0"')
|
|
59
|
-
expect(html).toContain(">example-workspace<")
|
|
60
|
-
expect(html).toContain(">clients/client-one<")
|
|
61
|
-
expect(html).toContain(`title="${CLIENT_ONE}"`)
|
|
62
|
-
expect(html.match(/lucide-folder /g)).toHaveLength(2)
|
|
63
|
-
expect(html).toContain('class="truncate">clients/client-one</span>')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test("shows the To domain favicon and a truncated message preview", () => {
|
|
67
|
-
const html = renderMailList([
|
|
68
|
-
{
|
|
69
|
-
...mailItem({ folder: CLIENT_ONE, id: "d_preview", subject: "Draft" }),
|
|
70
|
-
to: ["Ed <ed@example.com>"],
|
|
71
|
-
},
|
|
72
|
-
])
|
|
73
|
-
|
|
74
|
-
expect(html).toContain(
|
|
75
|
-
"https://www.google.com/s2/favicons?domain=example.com&sz=32",
|
|
76
|
-
)
|
|
77
|
-
expect(html).not.toContain("domain=ed")
|
|
78
|
-
expect(html).toContain('referrerPolicy="no-referrer"')
|
|
79
|
-
expect(html).toContain("grid-cols-[2rem_minmax(0,1fr)]")
|
|
80
|
-
expect(html).toMatch(/<p class="[^"]*truncate[^"]*">Draft excerpt<\/p>/)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
test("uses a mail icon without a favicon request for missing or invalid To addresses", () => {
|
|
84
|
-
for (const to of [[], ["invalid"], ["user@localhost"]]) {
|
|
85
|
-
const html = renderMailList([
|
|
86
|
-
{
|
|
87
|
-
...mailItem({ folder: null, id: "d_fallback", subject: "Draft" }),
|
|
88
|
-
to,
|
|
89
|
-
cc: ["cc@example.com"],
|
|
90
|
-
excerpt: "",
|
|
91
|
-
},
|
|
92
|
-
])
|
|
93
|
-
|
|
94
|
-
expect(html).toContain("lucide-mail")
|
|
95
|
-
expect(html).not.toContain("s2/favicons")
|
|
96
|
-
expect(html).not.toMatch(/<p[\s>]/)
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
test("time precedes the recipient in a left-aligned row", () => {
|
|
101
|
-
const html = renderMailList([
|
|
102
|
-
{
|
|
103
|
-
...mailItem({ folder: CLIENT_ONE, id: "d_time", subject: "Draft" }),
|
|
104
|
-
updatedAt: new Date().toISOString(),
|
|
105
|
-
},
|
|
106
|
-
])
|
|
107
|
-
|
|
108
|
-
expect(html).toContain("just now")
|
|
109
|
-
expect(html.indexOf("just now")).toBeLessThan(html.indexOf("To:"))
|
|
110
|
-
expect(html).toContain('class="flex items-start gap-2"')
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
describe("mailItemContext", () => {
|
|
115
|
-
test("formats one draft's fields into an agent-friendly prompt", () => {
|
|
116
|
-
const item = mailItem({
|
|
117
|
-
folder: WORKSPACE,
|
|
118
|
-
id: "d_ctx",
|
|
119
|
-
subject: "Pricing follow-up",
|
|
120
|
-
})
|
|
121
|
-
const text = mailItemContext(item, "Hi Ed,\n\nConfirming the numbers.")
|
|
122
|
-
|
|
123
|
-
expect(text).toContain("Instructions for the LLM:")
|
|
124
|
-
expect(text).toContain("- Subject: Pricing follow-up")
|
|
125
|
-
expect(text).toContain("- To: ed@example.com")
|
|
126
|
-
expect(text).toContain("- Status: working")
|
|
127
|
-
expect(text).toContain(`- Draft file: /tmp/d_ctx.md`)
|
|
128
|
-
expect(text).toContain("Draft body:")
|
|
129
|
-
expect(text).toContain("Confirming the numbers.")
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
test("falls back to the excerpt and a no-subject placeholder", () => {
|
|
133
|
-
const item = mailItem({ folder: null, id: "d_none", subject: null })
|
|
134
|
-
const text = mailItemContext(item)
|
|
135
|
-
|
|
136
|
-
expect(text).toContain("Subject: (no subject)")
|
|
137
|
-
expect(text).toContain("Draft excerpt:")
|
|
138
|
-
expect(text).toContain("Draft excerpt")
|
|
139
|
-
expect(text).not.toContain("Draft body:")
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
describe("MailFolderFilter markup", () => {
|
|
144
|
-
const drafts = [
|
|
145
|
-
WORKSPACE,
|
|
146
|
-
WORKSPACE,
|
|
147
|
-
CLIENT_ONE,
|
|
148
|
-
CLIENT_TWO,
|
|
149
|
-
CLIENT_THREE,
|
|
150
|
-
].map((folder, index) => ({ folder, id: `d_${index}` }))
|
|
151
|
-
const choices = folderChoices(drafts, WORKSPACE)
|
|
152
|
-
|
|
153
|
-
test("one trigger button shows the total when nothing is selected", () => {
|
|
154
|
-
const html = renderToStaticMarkup(
|
|
155
|
-
h(MailFolderFilter, {
|
|
156
|
-
choices,
|
|
157
|
-
onSelect: () => {},
|
|
158
|
-
selected: [],
|
|
159
|
-
total: drafts.length,
|
|
160
|
-
}),
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
// Single trigger with accessible name and the unfiltered total badge.
|
|
164
|
-
expect(html).toContain('aria-label="Filter drafts by folder"')
|
|
165
|
-
expect(html).toContain("lucide-folder-search")
|
|
166
|
-
expect(html).toContain(">Folder<")
|
|
167
|
-
expect(html).toContain(">5<")
|
|
168
|
-
// The All folders row lives inside the portal, so no SSR markup carries it.
|
|
169
|
-
expect(html).not.toContain("All folders")
|
|
170
|
-
// No pressed chips outside the portal dropdown.
|
|
171
|
-
const pressed = html.match(/aria-pressed=/g) ?? []
|
|
172
|
-
expect(pressed.length).toBe(0)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
test("a single selection shows the count on the trigger", () => {
|
|
176
|
-
const html = renderToStaticMarkup(
|
|
177
|
-
h(MailFolderFilter, {
|
|
178
|
-
choices,
|
|
179
|
-
onSelect: () => {},
|
|
180
|
-
selected: [CLIENT_ONE],
|
|
181
|
-
total: drafts.length,
|
|
182
|
-
}),
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
// Same single trigger; the badge shows the selected count.
|
|
186
|
-
expect(html).toContain('aria-label="Filter drafts by folder"')
|
|
187
|
-
expect(html).toContain(">Folder<")
|
|
188
|
-
expect(html).toContain(">1<")
|
|
189
|
-
expect(html).not.toContain("lucide-x")
|
|
190
|
-
expect(html).not.toContain("All folders")
|
|
191
|
-
})
|
|
192
|
-
})
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
-
"extends": "@raggle-ai/tsconfig/base.json",
|
|
4
|
-
"compilerOptions": {
|
|
5
|
-
"target": "ES2022",
|
|
6
|
-
"lib": ["dom", "dom.iterable", "esnext"],
|
|
7
|
-
"jsx": "preserve",
|
|
8
|
-
"baseUrl": ".",
|
|
9
|
-
"allowImportingTsExtensions": true,
|
|
10
|
-
// local copy: bun doesn't read paths inherited via `extends`
|
|
11
|
-
"paths": {
|
|
12
|
-
"~/*": ["${configDir}/../../packages/*/src"]
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"include": ["src"],
|
|
16
|
-
"exclude": ["node_modules", "dist", "eslint.config.mjs"]
|
|
17
|
-
}
|