kennel 0.2.2 → 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 +29 -3
- package/dist/cli/src/plugins/external.js +78 -24
- 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 +29 -3
- package/dist/plugins/external.js +78 -24
- 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,460 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto"
|
|
2
|
-
import {
|
|
3
|
-
mkdir,
|
|
4
|
-
mkdtemp,
|
|
5
|
-
readFile,
|
|
6
|
-
rm,
|
|
7
|
-
symlink,
|
|
8
|
-
writeFile,
|
|
9
|
-
} from "node:fs/promises"
|
|
10
|
-
import { tmpdir } from "node:os"
|
|
11
|
-
import { join } from "node:path"
|
|
12
|
-
import { describe, expect, test } from "bun:test"
|
|
13
|
-
|
|
14
|
-
import { deleteLocalDraftVerified, MailDeleteError } from "../src/history.ts"
|
|
15
|
-
|
|
16
|
-
function sha256(content) {
|
|
17
|
-
return createHash("sha256").update(content).digest("hex")
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function fixture() {
|
|
21
|
-
const base = await mkdtemp(join(tmpdir(), "kennel-history-"))
|
|
22
|
-
const mailDir = join(base, "drafts")
|
|
23
|
-
await mkdir(mailDir, { recursive: true })
|
|
24
|
-
return { base, mailDir }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function writeDraft(mailDir, id, content) {
|
|
28
|
-
const filePath = join(mailDir, `${id}.md`)
|
|
29
|
-
await writeFile(filePath, content)
|
|
30
|
-
return { filePath, revision: sha256(content), content }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function cleanup(base) {
|
|
34
|
-
await rm(base, { recursive: true, force: true })
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe("deleteLocalDraftVerified", () => {
|
|
38
|
-
test("snapshots byte-exact history then deletes the draft", async () => {
|
|
39
|
-
const { base, mailDir } = await fixture()
|
|
40
|
-
try {
|
|
41
|
-
const content = "---\nid: d_aaaaaa\nstatus: working\n---\n\nBody\n"
|
|
42
|
-
const { filePath, revision } = await writeDraft(
|
|
43
|
-
mailDir,
|
|
44
|
-
"d_aaaaaa",
|
|
45
|
-
content,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
const outcome = await deleteLocalDraftVerified(
|
|
49
|
-
{ id: "d_aaaaaa", filePath, folder: mailDir, revision },
|
|
50
|
-
mailDir,
|
|
51
|
-
)
|
|
52
|
-
expect(outcome).toBe("deleted")
|
|
53
|
-
|
|
54
|
-
const snapshot = await readFile(
|
|
55
|
-
join(mailDir, "history", "d_aaaaaa", `${revision}.md`),
|
|
56
|
-
"utf8",
|
|
57
|
-
)
|
|
58
|
-
expect(snapshot).toBe(content)
|
|
59
|
-
await expect(readFile(filePath, "utf8")).rejects.toThrow()
|
|
60
|
-
} finally {
|
|
61
|
-
await cleanup(base)
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
test("uses MailOS's lock file layout .locks/<id>.md.lock", async () => {
|
|
66
|
-
const { base, mailDir } = await fixture()
|
|
67
|
-
try {
|
|
68
|
-
const content = "---\nid: d_lockabc\nstatus: ready\n---\n\nBody\n"
|
|
69
|
-
const { filePath, revision } = await writeDraft(
|
|
70
|
-
mailDir,
|
|
71
|
-
"d_lockabc",
|
|
72
|
-
content,
|
|
73
|
-
)
|
|
74
|
-
await deleteLocalDraftVerified(
|
|
75
|
-
{ id: "d_lockabc", filePath, folder: mailDir, revision },
|
|
76
|
-
mailDir,
|
|
77
|
-
)
|
|
78
|
-
// MailOS holds `.locks/<basename>.lock` with `<basename>` = "<id>.md".
|
|
79
|
-
const lockPath = join(mailDir, ".locks", "d_lockabc.md.lock")
|
|
80
|
-
await expect(readFile(lockPath, "utf8")).resolves.toBe("")
|
|
81
|
-
} finally {
|
|
82
|
-
await cleanup(base)
|
|
83
|
-
}
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test("refuses to delete when the file changed after the loaded revision", async () => {
|
|
87
|
-
const { base, mailDir } = await fixture()
|
|
88
|
-
try {
|
|
89
|
-
const old = "---\nid: d_bbbbbb\nstatus: working\n---\n\nOld\n"
|
|
90
|
-
const { filePath, revision } = await writeDraft(mailDir, "d_bbbbbb", old)
|
|
91
|
-
await writeFile(
|
|
92
|
-
filePath,
|
|
93
|
-
"---\nid: d_bbbbbb\nstatus: working\n---\n\nNewer work\n",
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
await expect(
|
|
97
|
-
deleteLocalDraftVerified(
|
|
98
|
-
{ id: "d_bbbbbb", filePath, folder: mailDir, revision },
|
|
99
|
-
mailDir,
|
|
100
|
-
),
|
|
101
|
-
).rejects.toMatchObject({ name: "MailDeleteError", outcome: "changed" })
|
|
102
|
-
// The newer work must be untouched.
|
|
103
|
-
const current = await readFile(filePath, "utf8")
|
|
104
|
-
expect(current).toContain("Newer work")
|
|
105
|
-
} finally {
|
|
106
|
-
await cleanup(base)
|
|
107
|
-
}
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test("does not unlink when the file is missing", async () => {
|
|
111
|
-
const { base, mailDir } = await fixture()
|
|
112
|
-
try {
|
|
113
|
-
const { filePath } = await writeDraft(mailDir, "d_cccccc", "content")
|
|
114
|
-
await rm(filePath)
|
|
115
|
-
await expect(
|
|
116
|
-
deleteLocalDraftVerified(
|
|
117
|
-
{
|
|
118
|
-
id: "d_cccccc",
|
|
119
|
-
filePath,
|
|
120
|
-
folder: mailDir,
|
|
121
|
-
revision: sha256("content"),
|
|
122
|
-
},
|
|
123
|
-
mailDir,
|
|
124
|
-
),
|
|
125
|
-
).rejects.toMatchObject({ name: "MailDeleteError", outcome: "missing" })
|
|
126
|
-
} finally {
|
|
127
|
-
await cleanup(base)
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test("rejects traversal or invalid ids", async () => {
|
|
132
|
-
const { base, mailDir } = await fixture()
|
|
133
|
-
try {
|
|
134
|
-
const { revision } = await writeDraft(mailDir, "d_dddddd", "x")
|
|
135
|
-
for (const id of ["../d_dddddd", "a/b", "..", "."]) {
|
|
136
|
-
await expect(
|
|
137
|
-
deleteLocalDraftVerified(
|
|
138
|
-
{
|
|
139
|
-
id,
|
|
140
|
-
filePath: join(mailDir, `${id}.md`),
|
|
141
|
-
folder: mailDir,
|
|
142
|
-
revision,
|
|
143
|
-
},
|
|
144
|
-
mailDir,
|
|
145
|
-
),
|
|
146
|
-
).rejects.toMatchObject({ name: "MailDeleteError", outcome: "invalid" })
|
|
147
|
-
}
|
|
148
|
-
} finally {
|
|
149
|
-
await cleanup(base)
|
|
150
|
-
}
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
test("rejects a draft path outside the mail folder", async () => {
|
|
154
|
-
const { base, mailDir } = await fixture()
|
|
155
|
-
try {
|
|
156
|
-
const outside = join(base, "elsewhere.md")
|
|
157
|
-
await writeFile(outside, "x")
|
|
158
|
-
await expect(
|
|
159
|
-
deleteLocalDraftVerified(
|
|
160
|
-
{
|
|
161
|
-
id: "d_eeeeee",
|
|
162
|
-
filePath: outside,
|
|
163
|
-
folder: mailDir,
|
|
164
|
-
revision: sha256("x"),
|
|
165
|
-
},
|
|
166
|
-
mailDir,
|
|
167
|
-
),
|
|
168
|
-
).rejects.toMatchObject({ name: "MailDeleteError", outcome: "invalid" })
|
|
169
|
-
} finally {
|
|
170
|
-
await cleanup(base)
|
|
171
|
-
}
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
test("rejects a symlinked draft file instead of following it", async () => {
|
|
175
|
-
const { base, mailDir } = await fixture()
|
|
176
|
-
try {
|
|
177
|
-
const target = join(base, "secret.md")
|
|
178
|
-
await writeFile(target, "outside content")
|
|
179
|
-
const link = join(mailDir, "d_ffffff.md")
|
|
180
|
-
await symlink(target, link)
|
|
181
|
-
|
|
182
|
-
await expect(
|
|
183
|
-
deleteLocalDraftVerified(
|
|
184
|
-
{
|
|
185
|
-
id: "d_ffffff",
|
|
186
|
-
filePath: link,
|
|
187
|
-
folder: mailDir,
|
|
188
|
-
revision: sha256("outside content"),
|
|
189
|
-
},
|
|
190
|
-
mailDir,
|
|
191
|
-
),
|
|
192
|
-
).rejects.toMatchObject({ name: "MailDeleteError", outcome: "invalid" })
|
|
193
|
-
// The symlink and its target both survive.
|
|
194
|
-
await expect(readFile(link, "utf8")).resolves.toBe("outside content")
|
|
195
|
-
await expect(readFile(target, "utf8")).resolves.toBe("outside content")
|
|
196
|
-
} finally {
|
|
197
|
-
await cleanup(base)
|
|
198
|
-
}
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
test("rejects a symlinked history directory instead of following it", async () => {
|
|
202
|
-
const { base, mailDir } = await fixture()
|
|
203
|
-
try {
|
|
204
|
-
const target = join(base, "elsewhere")
|
|
205
|
-
await mkdir(target, { recursive: true })
|
|
206
|
-
await symlink(target, join(mailDir, "history"))
|
|
207
|
-
|
|
208
|
-
const content = "---\nid: d_histlink\nstatus: working\n---\n\nBody\n"
|
|
209
|
-
const { filePath, revision } = await writeDraft(
|
|
210
|
-
mailDir,
|
|
211
|
-
"d_histlink",
|
|
212
|
-
content,
|
|
213
|
-
)
|
|
214
|
-
await expect(
|
|
215
|
-
deleteLocalDraftVerified(
|
|
216
|
-
{ id: "d_histlink", filePath, folder: mailDir, revision },
|
|
217
|
-
mailDir,
|
|
218
|
-
),
|
|
219
|
-
).rejects.toMatchObject({ name: "MailDeleteError", outcome: "invalid" })
|
|
220
|
-
// The draft survives and nothing was written through the symlink.
|
|
221
|
-
await expect(readFile(filePath, "utf8")).resolves.toBe(content)
|
|
222
|
-
await expect(
|
|
223
|
-
readFile(join(target, `${revision}.md`), "utf8"),
|
|
224
|
-
).rejects.toThrow()
|
|
225
|
-
} finally {
|
|
226
|
-
await cleanup(base)
|
|
227
|
-
}
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
test("refuses to overwrite corrupt existing history and keeps the draft", async () => {
|
|
231
|
-
const { base, mailDir } = await fixture()
|
|
232
|
-
try {
|
|
233
|
-
const content = "---\nid: d_corrupt\nstatus: working\n---\n\nBody\n"
|
|
234
|
-
const { filePath, revision } = await writeDraft(
|
|
235
|
-
mailDir,
|
|
236
|
-
"d_corrupt",
|
|
237
|
-
content,
|
|
238
|
-
)
|
|
239
|
-
const historyDir = join(mailDir, "history", "d_corrupt")
|
|
240
|
-
await mkdir(historyDir, { recursive: true })
|
|
241
|
-
const historyPath = join(historyDir, `${revision}.md`)
|
|
242
|
-
await writeFile(historyPath, "corrupted bytes")
|
|
243
|
-
|
|
244
|
-
await expect(
|
|
245
|
-
deleteLocalDraftVerified(
|
|
246
|
-
{ id: "d_corrupt", filePath, folder: mailDir, revision },
|
|
247
|
-
mailDir,
|
|
248
|
-
),
|
|
249
|
-
).rejects.toMatchObject({ name: "MailDeleteError", outcome: "changed" })
|
|
250
|
-
// Neither the draft nor the (corrupt) history is touched.
|
|
251
|
-
await expect(readFile(filePath, "utf8")).resolves.toBe(content)
|
|
252
|
-
await expect(readFile(historyPath, "utf8")).resolves.toBe(
|
|
253
|
-
"corrupted bytes",
|
|
254
|
-
)
|
|
255
|
-
} finally {
|
|
256
|
-
await cleanup(base)
|
|
257
|
-
}
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
test("serializes concurrent deletes of the same draft through the real lock", async () => {
|
|
261
|
-
const { base, mailDir } = await fixture()
|
|
262
|
-
try {
|
|
263
|
-
const content = "---\nid: d_111111\nstatus: working\n---\n\nBody\n"
|
|
264
|
-
const { filePath, revision } = await writeDraft(
|
|
265
|
-
mailDir,
|
|
266
|
-
"d_111111",
|
|
267
|
-
content,
|
|
268
|
-
)
|
|
269
|
-
const identity = { id: "d_111111", filePath, folder: mailDir, revision }
|
|
270
|
-
|
|
271
|
-
const [first, second] = await Promise.allSettled([
|
|
272
|
-
deleteLocalDraftVerified(identity, mailDir),
|
|
273
|
-
deleteLocalDraftVerified(identity, mailDir),
|
|
274
|
-
])
|
|
275
|
-
const outcomes = [first, second].map((result) =>
|
|
276
|
-
result.status === "fulfilled" ? result.value : result.reason.outcome,
|
|
277
|
-
)
|
|
278
|
-
expect(outcomes.sort()).toEqual(["deleted", "missing"])
|
|
279
|
-
// Exactly one byte-exact snapshot exists.
|
|
280
|
-
const snapshot = await readFile(
|
|
281
|
-
join(mailDir, "history", "d_111111", `${revision}.md`),
|
|
282
|
-
"utf8",
|
|
283
|
-
)
|
|
284
|
-
expect(snapshot).toBe(content)
|
|
285
|
-
} finally {
|
|
286
|
-
await cleanup(base)
|
|
287
|
-
}
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
test("waits for a lock held by another process, then deletes", async () => {
|
|
291
|
-
const { base, mailDir } = await fixture()
|
|
292
|
-
let child = null
|
|
293
|
-
try {
|
|
294
|
-
const content = "---\nid: d_proc\nstatus: ready\n---\n\nBody\n"
|
|
295
|
-
const { filePath, revision } = await writeDraft(
|
|
296
|
-
mailDir,
|
|
297
|
-
"d_proc",
|
|
298
|
-
content,
|
|
299
|
-
)
|
|
300
|
-
const lockPath = join(mailDir, ".locks", "d_proc.md.lock")
|
|
301
|
-
// The child opens the lock file itself; openSync cannot create the
|
|
302
|
-
// parent directory, so create the real `.locks` dir first (exactly as
|
|
303
|
-
// deleteLocalDraftVerified's ensureRealDirChain would).
|
|
304
|
-
await mkdir(join(mailDir, ".locks"), { recursive: true })
|
|
305
|
-
|
|
306
|
-
// A separate OS process takes the MailOS lock for 800ms.
|
|
307
|
-
const childScript = `
|
|
308
|
-
import { openSync } from "node:fs"
|
|
309
|
-
import { dlopen } from "bun:ffi"
|
|
310
|
-
const lib = dlopen(process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6", { flock: { args: ["int", "int"], returns: "int" } })
|
|
311
|
-
const flock = lib.symbols.flock
|
|
312
|
-
const fd = openSync(${JSON.stringify(lockPath)}, "a+", 0o600)
|
|
313
|
-
flock(fd, 2)
|
|
314
|
-
console.log("CHILD_LOCKED")
|
|
315
|
-
await Bun.sleep(800)
|
|
316
|
-
flock(fd, 8)
|
|
317
|
-
process.exit(0)
|
|
318
|
-
`
|
|
319
|
-
child = Bun.spawn({
|
|
320
|
-
cmd: ["bun", "-e", childScript],
|
|
321
|
-
cwd: import.meta.dirname,
|
|
322
|
-
stdout: "pipe",
|
|
323
|
-
stderr: "pipe",
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
// Wait until the foreign process actually holds the flock, otherwise
|
|
327
|
-
// the delete could legitimately win the race. The decoded streamed
|
|
328
|
-
// chunks are accumulated: byte-comparing the raw Uint8Array never
|
|
329
|
-
// matches, and a marker split across chunks only matches once the text
|
|
330
|
-
// accumulates. Bounded so a silent child cannot hang the suite.
|
|
331
|
-
await waitForChildSignal(child, "CHILD_LOCKED")
|
|
332
|
-
|
|
333
|
-
const startedAt = performance.now()
|
|
334
|
-
const outcome = await deleteLocalDraftVerified(
|
|
335
|
-
{ id: "d_proc", filePath, folder: mailDir, revision },
|
|
336
|
-
mailDir,
|
|
337
|
-
)
|
|
338
|
-
const elapsed = performance.now() - startedAt
|
|
339
|
-
|
|
340
|
-
expect(outcome).toBe("deleted")
|
|
341
|
-
// The delete had to wait for the foreign holder to release.
|
|
342
|
-
expect(elapsed).toBeGreaterThanOrEqual(600)
|
|
343
|
-
const snapshot = await readFile(
|
|
344
|
-
join(mailDir, "history", "d_proc", `${revision}.md`),
|
|
345
|
-
"utf8",
|
|
346
|
-
)
|
|
347
|
-
expect(snapshot).toBe(content)
|
|
348
|
-
expect(await childExitedBounded(child)).toBe(0)
|
|
349
|
-
} finally {
|
|
350
|
-
if (child) killIfRunning(child)
|
|
351
|
-
await cleanup(base)
|
|
352
|
-
}
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
test("rejects a malformed revision", async () => {
|
|
356
|
-
const { base, mailDir } = await fixture()
|
|
357
|
-
try {
|
|
358
|
-
const { filePath } = await writeDraft(mailDir, "d_222222", "x")
|
|
359
|
-
await expect(
|
|
360
|
-
deleteLocalDraftVerified(
|
|
361
|
-
{ id: "d_222222", filePath, folder: mailDir, revision: "not-a-hash" },
|
|
362
|
-
mailDir,
|
|
363
|
-
),
|
|
364
|
-
).rejects.toMatchObject({ name: "MailDeleteError", outcome: "invalid" })
|
|
365
|
-
} finally {
|
|
366
|
-
await cleanup(base)
|
|
367
|
-
}
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
test("delete is idempotent for an already-snapshotted revision", async () => {
|
|
371
|
-
const { base, mailDir } = await fixture()
|
|
372
|
-
try {
|
|
373
|
-
const content = "---\nid: d_333333\nstatus: working\n---\n\nBody\n"
|
|
374
|
-
const { filePath, revision } = await writeDraft(
|
|
375
|
-
mailDir,
|
|
376
|
-
"d_333333",
|
|
377
|
-
content,
|
|
378
|
-
)
|
|
379
|
-
const identity = { id: "d_333333", filePath, folder: mailDir, revision }
|
|
380
|
-
expect(await deleteLocalDraftVerified(identity, mailDir)).toBe("deleted")
|
|
381
|
-
// Second attempt: no file, no duplicate snapshot overwrite concerns.
|
|
382
|
-
await expect(
|
|
383
|
-
deleteLocalDraftVerified(identity, mailDir),
|
|
384
|
-
).rejects.toMatchObject({
|
|
385
|
-
outcome: "missing",
|
|
386
|
-
})
|
|
387
|
-
} finally {
|
|
388
|
-
await cleanup(base)
|
|
389
|
-
}
|
|
390
|
-
})
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
test("MailDeleteError is an Error subclass", () => {
|
|
394
|
-
const error = new MailDeleteError("changed", "nope")
|
|
395
|
-
expect(error).toBeInstanceOf(Error)
|
|
396
|
-
expect(error.outcome).toBe("changed")
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Read a spawned child's stdout stream and resolve once the decoded,
|
|
401
|
-
* accumulated text contains `marker`. Chunks are decoded incrementally with a
|
|
402
|
-
* streaming TextDecoder and concatenated; byte-comparing the Uint8Array or
|
|
403
|
-
* skipping the accumulation step would never match across chunk boundaries.
|
|
404
|
-
* Bounded so a silent child cannot hang the suite.
|
|
405
|
-
*/
|
|
406
|
-
function waitForChildSignal(child, marker) {
|
|
407
|
-
return new Promise((resolveSignal, rejectSignal) => {
|
|
408
|
-
const reader = child.stdout.getReader()
|
|
409
|
-
const decoder = new TextDecoder()
|
|
410
|
-
let collected = ""
|
|
411
|
-
const failTimer = setTimeout(
|
|
412
|
-
() =>
|
|
413
|
-
rejectSignal(new Error(`timeout waiting for child marker ${marker}`)),
|
|
414
|
-
10_000,
|
|
415
|
-
)
|
|
416
|
-
void (async () => {
|
|
417
|
-
try {
|
|
418
|
-
for (;;) {
|
|
419
|
-
const { done, value } = await reader.read()
|
|
420
|
-
if (done) break
|
|
421
|
-
collected += decoder.decode(value, { stream: true })
|
|
422
|
-
if (collected.includes(marker)) {
|
|
423
|
-
clearTimeout(failTimer)
|
|
424
|
-
resolveSignal()
|
|
425
|
-
return
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
clearTimeout(failTimer)
|
|
429
|
-
rejectSignal(new Error(`child closed stdout without marker ${marker}`))
|
|
430
|
-
} catch (error) {
|
|
431
|
-
clearTimeout(failTimer)
|
|
432
|
-
rejectSignal(error)
|
|
433
|
-
}
|
|
434
|
-
})()
|
|
435
|
-
})
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
async function childExitedBounded(child, ms = 10_000) {
|
|
439
|
-
return Promise.race([
|
|
440
|
-
child.exited,
|
|
441
|
-
new Promise((_, rejectExited) =>
|
|
442
|
-
setTimeout(
|
|
443
|
-
() => rejectExited(new Error("child did not exit in time")),
|
|
444
|
-
ms,
|
|
445
|
-
),
|
|
446
|
-
),
|
|
447
|
-
]).catch(async (error) => {
|
|
448
|
-
killIfRunning(child)
|
|
449
|
-
await child.exited.catch(() => undefined)
|
|
450
|
-
throw error
|
|
451
|
-
})
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function killIfRunning(child) {
|
|
455
|
-
try {
|
|
456
|
-
if (child.exitCode === null) child.kill()
|
|
457
|
-
} catch {
|
|
458
|
-
// Already gone.
|
|
459
|
-
}
|
|
460
|
-
}
|