create-harness-vibe-coding 0.8.19 → 0.9.3
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/CHANGELOG.md +49 -0
- package/README-CN.md +4 -2
- package/README.md +23 -4
- package/package.json +17 -2
- package/src/generator.js +19 -6
- package/src/index.js +531 -38
- package/src/prompts.js +4 -4
- package/src/ui/dist/assets/AgentsRoute-BGGs90wH.js +7 -0
- package/src/ui/dist/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
- package/src/ui/dist/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
- package/src/ui/dist/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
- package/src/ui/dist/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
- package/src/ui/dist/assets/Combination-DhBLEmof.js +42 -0
- package/src/ui/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/src/ui/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/src/ui/dist/assets/RolesRoute-DnodCSz0.js +2 -0
- package/src/ui/dist/assets/RuntimePicker-CucD8TGn.js +7 -0
- package/src/ui/dist/assets/SettingsRoute-DquMWjac.js +2 -0
- package/src/ui/dist/assets/TaskList-Bxc0Uktx.js +7 -0
- package/src/ui/dist/assets/TerminalDrawer-CDusYltc.js +36 -0
- package/src/ui/dist/assets/WorkflowRoute-BN8yHmUs.css +1 -0
- package/src/ui/dist/assets/WorkflowRoute-D-0wVDpY.js +334 -0
- package/src/ui/dist/assets/abnfDiagram-VRR7QNED-zkRKkaxO.js +2 -0
- package/src/ui/dist/assets/apl-B4CMkyY2.js +2 -0
- package/src/ui/dist/assets/ar-SA-G6X2FPQ2-BiHaArx5.js +11 -0
- package/src/ui/dist/assets/arc-CBnLtOFO.js +2 -0
- package/src/ui/dist/assets/architectureDiagram-ZJ3FMSHR-DWXRXsh1.js +37 -0
- package/src/ui/dist/assets/asciiarmor-Df11BRmG.js +2 -0
- package/src/ui/dist/assets/asn1-EdZsLKOL.js +2 -0
- package/src/ui/dist/assets/asterisk-B-8jnY81.js +2 -0
- package/src/ui/dist/assets/az-AZ-76LH7QW2-CiT05voO.js +2 -0
- package/src/ui/dist/assets/bg-BG-XCXSNQG7-L-UM9tNf.js +6 -0
- package/src/ui/dist/assets/blockDiagram-677ZJIJ3-BIyh3Bry.js +133 -0
- package/src/ui/dist/assets/bn-BD-2XOGV67Q-NdgbA0bC.js +6 -0
- package/src/ui/dist/assets/brainfuck-C4LP7Hcl.js +2 -0
- package/src/ui/dist/assets/c4Diagram-LMCZKHZV-CA54vWWM.js +11 -0
- package/src/ui/dist/assets/ca-ES-6MX7JW3Y-BKZZsQ5J.js +9 -0
- package/src/ui/dist/assets/channel-Da_-zcAu.js +2 -0
- package/src/ui/dist/assets/chunk-2Q5K7J3B-DE4cxr1v.js +2 -0
- package/src/ui/dist/assets/chunk-32BRIVSS-i7FwbEU9.js +2 -0
- package/src/ui/dist/assets/chunk-5VM5RSS4-BhIcOYSu.js +16 -0
- package/src/ui/dist/assets/chunk-EX3LRPZG-Ci9zZRIs.js +232 -0
- package/src/ui/dist/assets/chunk-JWPE2WC7-BFdkcz-G.js +2 -0
- package/src/ui/dist/assets/chunk-MOJQB5TN-Dzmapl_S.js +89 -0
- package/src/ui/dist/assets/chunk-RYQCIY6F-RTmp7FOy.js +2 -0
- package/src/ui/dist/assets/chunk-V7JOEXUC-DInU3v3D.js +207 -0
- package/src/ui/dist/assets/chunk-VR4S4FIN-BHoyGBQO.js +2 -0
- package/src/ui/dist/assets/chunk-XXDRQBXY-DfeLZNZx.js +2 -0
- package/src/ui/dist/assets/classDiagram-OUVF2IWQ-FKbUS0bu.js +2 -0
- package/src/ui/dist/assets/classDiagram-v2-EOCWNBFH-FKbUS0bu.js +2 -0
- package/src/ui/dist/assets/clike-B9uivgTg.js +2 -0
- package/src/ui/dist/assets/clojure-BMjYHr_A.js +2 -0
- package/src/ui/dist/assets/cmake-BQqOBYOt.js +2 -0
- package/src/ui/dist/assets/cobol-CWcv1MsR.js +2 -0
- package/src/ui/dist/assets/coffeescript-S37ZYGWr.js +2 -0
- package/src/ui/dist/assets/commonlisp-DBKNyK5s.js +2 -0
- package/src/ui/dist/assets/cose-bilkent-JH36ORCC-_5PUJ615.js +2 -0
- package/src/ui/dist/assets/crystal-SjHAIU92.js +2 -0
- package/src/ui/dist/assets/cs-CZ-2BRQDIVT-DnkfnhDU.js +12 -0
- package/src/ui/dist/assets/css-BnMrqG3P.js +2 -0
- package/src/ui/dist/assets/cynefin-VYW2F7L2-CmkcFv-b.js +179 -0
- package/src/ui/dist/assets/cynefinDiagram-TSTJHNR4-D72VZn8H.js +63 -0
- package/src/ui/dist/assets/cypher-C_CwsFkJ.js +2 -0
- package/src/ui/dist/assets/cytoscape.esm-DTSO7Bv0.js +332 -0
- package/src/ui/dist/assets/d-pRatUO7H.js +2 -0
- package/src/ui/dist/assets/da-DK-5WZEPLOC-C54tQUgd.js +6 -0
- package/src/ui/dist/assets/dagre-VKFMJZFB-0BsAkrmo.js +5 -0
- package/src/ui/dist/assets/de-DE-XR44H4JA-DGfZlR10.js +9 -0
- package/src/ui/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
- package/src/ui/dist/assets/diagram-FQU43EPY-C1ZJDOca.js +4 -0
- package/src/ui/dist/assets/diagram-G47NLZAW-D3pr3FSP.js +25 -0
- package/src/ui/dist/assets/diagram-NH7WQ7WH-Bwry0NV-.js +25 -0
- package/src/ui/dist/assets/diagram-OA4YK3LP-L6g09cmV.js +31 -0
- package/src/ui/dist/assets/diagram-WEI45ONY-Bh1puYVX.js +42 -0
- package/src/ui/dist/assets/diff-DbItnlRl.js +2 -0
- package/src/ui/dist/assets/directory-open-01563666-DWU9wJ6I.js +2 -0
- package/src/ui/dist/assets/directory-open-4ed118d0-CunoC1EB.js +2 -0
- package/src/ui/dist/assets/dockerfile-BKs6k2Af.js +2 -0
- package/src/ui/dist/assets/dtd-DF_7sFjM.js +2 -0
- package/src/ui/dist/assets/dylan-DwRh75JA.js +2 -0
- package/src/ui/dist/assets/ebnf-CDyGwa7X.js +2 -0
- package/src/ui/dist/assets/ebnfDiagram-CCIWWBDH-BC_mBRR5.js +2 -0
- package/src/ui/dist/assets/ecl-Cabwm37j.js +2 -0
- package/src/ui/dist/assets/eiffel-CnydiIhH.js +2 -0
- package/src/ui/dist/assets/el-GR-BZB4AONW-DsnwWweL.js +11 -0
- package/src/ui/dist/assets/elm-vLlmbW-K.js +2 -0
- package/src/ui/dist/assets/erDiagram-Q63AITRT-Bkxam36l.js +86 -0
- package/src/ui/dist/assets/erlang-BNw1qcRV.js +2 -0
- package/src/ui/dist/assets/es-ES-U4NZUMDT-CRxBcQgC.js +10 -0
- package/src/ui/dist/assets/eu-ES-A7QVB2H4-CTd_E7YR.js +12 -0
- package/src/ui/dist/assets/excalidraw-default-bMWIgNCy.svg +1 -0
- package/src/ui/dist/assets/fa-IR-HGAKTJCU-DO56k7eG.js +9 -0
- package/src/ui/dist/assets/factor-kuTfRLto.js +2 -0
- package/src/ui/dist/assets/fcl-Kvtd6kyn.js +2 -0
- package/src/ui/dist/assets/fi-FI-Z5N7JZ37-DEDCF9WB.js +7 -0
- package/src/ui/dist/assets/file-open-002ab408-DIuFHtCF.js +2 -0
- package/src/ui/dist/assets/file-open-7c801643-684qeFg4.js +2 -0
- package/src/ui/dist/assets/file-save-3189631c-C1wFhQhH.js +2 -0
- package/src/ui/dist/assets/file-save-745eba88-Bb9F9Kg7.js +2 -0
- package/src/ui/dist/assets/flowDiagram-23GEKE2U-C3SAEao7.js +157 -0
- package/src/ui/dist/assets/forth-Ffai-XNe.js +2 -0
- package/src/ui/dist/assets/fortran-DYz_wnZ1.js +2 -0
- package/src/ui/dist/assets/fr-FR-RHASNOE6-DE2nPyDt.js +10 -0
- package/src/ui/dist/assets/ganttDiagram-NO4QXBWP-U-J_Xi-_.js +293 -0
- package/src/ui/dist/assets/gas-Bneqetm1.js +2 -0
- package/src/ui/dist/assets/gherkin-heZmZLOM.js +2 -0
- package/src/ui/dist/assets/gitGraphDiagram-IHSO6WYX-BADVbD-a.js +107 -0
- package/src/ui/dist/assets/gl-ES-HMX3MZ6V-mCqVgQHC.js +11 -0
- package/src/ui/dist/assets/graph-C9eacEi8.js +2 -0
- package/src/ui/dist/assets/groovy-D9Dt4D0W.js +2 -0
- package/src/ui/dist/assets/haskell-Cw1EW3IL.js +2 -0
- package/src/ui/dist/assets/haxe-H-WmDvRZ.js +2 -0
- package/src/ui/dist/assets/he-IL-6SHJWFNN-KnaLrCzw.js +11 -0
- package/src/ui/dist/assets/hi-IN-IWLTKZ5I-wuqiaaWt.js +5 -0
- package/src/ui/dist/assets/http-DBlCnlav.js +2 -0
- package/src/ui/dist/assets/hu-HU-A5ZG7DT2-DLg_zm22.js +8 -0
- package/src/ui/dist/assets/id-ID-SAP4L64H-MB2mXqcR.js +11 -0
- package/src/ui/dist/assets/idl-BEugSyMb.js +2 -0
- package/src/ui/dist/assets/image-blob-reduce.esm-HwCm7IZ0.js +8 -0
- package/src/ui/dist/assets/index-39SJtrt0.js +2 -0
- package/src/ui/dist/assets/index-BQ0LIrW-.js +2 -0
- package/src/ui/dist/assets/index-BcAUh2Nm.css +1 -0
- package/src/ui/dist/assets/index-BcThOK-i.js +331 -0
- package/src/ui/dist/assets/index-BnxV3k_S.js +3 -0
- package/src/ui/dist/assets/index-C7MZBHbT.js +117 -0
- package/src/ui/dist/assets/index-CCvKI2rM.js +2 -0
- package/src/ui/dist/assets/index-CZvmcCab.js +8 -0
- package/src/ui/dist/assets/index-Cp3hLKNl.js +2 -0
- package/src/ui/dist/assets/index-D7ayFxcx.js +2 -0
- package/src/ui/dist/assets/index-DAYxSQGU.js +2 -0
- package/src/ui/dist/assets/index-DD-HeMV0.css +1 -0
- package/src/ui/dist/assets/index-DPMxd3Qn.js +2 -0
- package/src/ui/dist/assets/index-DSe2jx3k.js +4 -0
- package/src/ui/dist/assets/index-DUirCkgW.js +2 -0
- package/src/ui/dist/assets/index-DaKPQ2hN.js +159 -0
- package/src/ui/dist/assets/index-DzpuR0Jg.js +2 -0
- package/src/ui/dist/assets/index-POndwsCI.js +2 -0
- package/src/ui/dist/assets/index-R8NM9I_9.js +2 -0
- package/src/ui/dist/assets/index-TCDuDhBj.js +2 -0
- package/src/ui/dist/assets/index-TckzASdM.js +2 -0
- package/src/ui/dist/assets/index-Yi5ytFFr.js +2 -0
- package/src/ui/dist/assets/infoDiagram-FWYZ7A6U-CiGBlFqH.js +3 -0
- package/src/ui/dist/assets/init-Gi6I4Gst.js +2 -0
- package/src/ui/dist/assets/ishikawaDiagram-FXEZZL3T-B1aheRbz.js +71 -0
- package/src/ui/dist/assets/it-IT-JPQ66NNP-DT3HSU7X.js +12 -0
- package/src/ui/dist/assets/ja-JP-DBVTYXUO-CsfJjjWt.js +9 -0
- package/src/ui/dist/assets/javascript-iXu5QeM3.js +2 -0
- package/src/ui/dist/assets/journeyDiagram-5HDEW3XC-DtnJPxub.js +140 -0
- package/src/ui/dist/assets/julia-DuME0IfC.js +2 -0
- package/src/ui/dist/assets/kaa-6HZHGXH3-DQz-1-Gn.js +2 -0
- package/src/ui/dist/assets/kab-KAB-ZGHBKWFO-CaH-LSoK.js +9 -0
- package/src/ui/dist/assets/kanban-definition-HUTT4EX6-CZatSm3e.js +90 -0
- package/src/ui/dist/assets/katex-C5jXJg4s.js +258 -0
- package/src/ui/dist/assets/kk-KZ-P5N5QNE5-BMXmcm55.js +2 -0
- package/src/ui/dist/assets/km-KH-HSX4SM5Z-ZeoJNBpN.js +12 -0
- package/src/ui/dist/assets/ko-KR-MTYHY66A-BeaLRKph.js +10 -0
- package/src/ui/dist/assets/ku-TR-6OUDTVRD-B_28KrkR.js +10 -0
- package/src/ui/dist/assets/layout-DEXfKzaS.js +2 -0
- package/src/ui/dist/assets/linear-CpEDnQRC.js +2 -0
- package/src/ui/dist/assets/livescript-BwQOo05w.js +2 -0
- package/src/ui/dist/assets/lt-LT-XHIRWOB4-BxDcnUhp.js +4 -0
- package/src/ui/dist/assets/lua-VAEuO923.js +2 -0
- package/src/ui/dist/assets/lv-LV-5QDEKY6T-DpKDl1_y.js +8 -0
- package/src/ui/dist/assets/map-Czzmt4hB.js +2 -0
- package/src/ui/dist/assets/mathematica-DTrFuWx2.js +2 -0
- package/src/ui/dist/assets/mbox-CNhZ1qSd.js +2 -0
- package/src/ui/dist/assets/mermaidLoader-CBO31t0J.js +305 -0
- package/src/ui/dist/assets/mermaidLoader-CEK31ho9.css +1 -0
- package/src/ui/dist/assets/mindmap-definition-LN4V7U3C-CQ0TccEr.js +97 -0
- package/src/ui/dist/assets/mirc-CjQqDB4T.js +2 -0
- package/src/ui/dist/assets/mllike-CXdrOF99.js +2 -0
- package/src/ui/dist/assets/modelica-Dc1JOy9r.js +2 -0
- package/src/ui/dist/assets/mr-IN-CRQNXWMA-BKCZT58p.js +14 -0
- package/src/ui/dist/assets/mscgen-BA5vi2Kp.js +2 -0
- package/src/ui/dist/assets/mumps-BT43cFF4.js +2 -0
- package/src/ui/dist/assets/my-MM-5M5IBNSE-D34k0G9T.js +2 -0
- package/src/ui/dist/assets/nb-NO-T6EIAALU-DXr_KW8J.js +11 -0
- package/src/ui/dist/assets/nginx-DdIZxoE0.js +2 -0
- package/src/ui/dist/assets/nl-NL-IS3SIHDZ-C1L0aVDR.js +9 -0
- package/src/ui/dist/assets/nn-NO-6E72VCQL-CLWVSxbZ.js +9 -0
- package/src/ui/dist/assets/nsis-LdVXkNf5.js +2 -0
- package/src/ui/dist/assets/ntriples-BfvgReVJ.js +2 -0
- package/src/ui/dist/assets/oc-FR-POXYY2M6-nf8_J3eR.js +9 -0
- package/src/ui/dist/assets/octave-Ck1zUtKM.js +2 -0
- package/src/ui/dist/assets/ordinal-Cboi1Yqb.js +2 -0
- package/src/ui/dist/assets/oz-BzwKVEFT.js +2 -0
- package/src/ui/dist/assets/pa-IN-N4M65BXN-CAMJ-xd6.js +5 -0
- package/src/ui/dist/assets/pascal--L3eBynH.js +2 -0
- package/src/ui/dist/assets/pegDiagram-2B236MQR-Bj2RcmRd.js +2 -0
- package/src/ui/dist/assets/percentages-BXMCSKIN-s9yWFdr6.js +160 -0
- package/src/ui/dist/assets/perl-CdXCOZ3F.js +2 -0
- package/src/ui/dist/assets/pica-BGv_AhAB.js +8 -0
- package/src/ui/dist/assets/pieDiagram-ENE6RG2P-obpzz8dC.js +40 -0
- package/src/ui/dist/assets/pig-CevX1Tat.js +2 -0
- package/src/ui/dist/assets/pl-PL-T2D74RX3-DKJ5H85v.js +10 -0
- package/src/ui/dist/assets/powershell-CFHJl5sT.js +2 -0
- package/src/ui/dist/assets/properties-C78fOPTZ.js +2 -0
- package/src/ui/dist/assets/protobuf-ChK-085T.js +2 -0
- package/src/ui/dist/assets/{proxy-D0vUbds5.js → proxy-BSmwef4H.js} +2 -2
- package/src/ui/dist/assets/pt-BR-5N22H2LF-B5kow-kN.js +10 -0
- package/src/ui/dist/assets/pt-PT-UZXXM6DQ-B2G34fso.js +10 -0
- package/src/ui/dist/assets/pug-DeIclll2.js +2 -0
- package/src/ui/dist/assets/puppet-DMA9R1ak.js +2 -0
- package/src/ui/dist/assets/python-BuPzkPfP.js +2 -0
- package/src/ui/dist/assets/q-pXgVlZs6.js +2 -0
- package/src/ui/dist/assets/quadrantDiagram-ABIIQ3AL-Cl-T7NkJ.js +8 -0
- package/src/ui/dist/assets/r-B6wPVr8A.js +2 -0
- package/src/ui/dist/assets/railroadDiagram-RFXS5EU6-DMq82RHg.js +2 -0
- package/src/ui/dist/assets/{refresh-cw-JNmExijd.js → refresh-cw-CbFd7guV.js} +2 -2
- package/src/ui/dist/assets/requirementDiagram-TGXJPOKE-tGInMDgU.js +85 -0
- package/src/ui/dist/assets/ro-RO-JPDTUUEW-BwLdZW91.js +12 -0
- package/src/ui/dist/assets/roundRect-0PYZxl1G.js +2 -0
- package/src/ui/dist/assets/rpm-CTu-6PCP.js +2 -0
- package/src/ui/dist/assets/ru-RU-B4JR7IUQ-DpdAgIJc.js +10 -0
- package/src/ui/dist/assets/ruby-B2Rjki9n.js +2 -0
- package/src/ui/dist/assets/runtimeBrand-BoKPimsz.js +2 -0
- package/src/ui/dist/assets/sankeyDiagram-HTMAVEWB-C7X-ET4m.js +41 -0
- package/src/ui/dist/assets/sas-B4kiWyti.js +2 -0
- package/src/ui/dist/assets/save-D1CXHwBw.js +12 -0
- package/src/ui/dist/assets/scheme-C41bIUwD.js +2 -0
- package/src/ui/dist/assets/sequenceDiagram-DBY2YBRQ-Bn24i_nY.js +163 -0
- package/src/ui/dist/assets/settings-2-DtSNAAcU.js +22 -0
- package/src/ui/dist/assets/shell-CjFT_Tl9.js +2 -0
- package/src/ui/dist/assets/shield-check-BVbJ8T_N.js +7 -0
- package/src/ui/dist/assets/si-LK-N5RQ5JYF-naJaUy5u.js +2 -0
- package/src/ui/dist/assets/sieve-C3Gn_uJK.js +2 -0
- package/src/ui/dist/assets/simple-mode-GW_nhZxv.js +2 -0
- package/src/ui/dist/assets/sizeCapture-X5ZJPWSS-DAQHfm82.js +2 -0
- package/src/ui/dist/assets/sk-SK-C5VTKIMK-1NPz1J_d.js +7 -0
- package/src/ui/dist/assets/sl-SI-NN7IZMDC-D8RfmNQw.js +7 -0
- package/src/ui/dist/assets/smalltalk-CnHTOXQT.js +2 -0
- package/src/ui/dist/assets/solr-DehyRSwq.js +2 -0
- package/src/ui/dist/assets/sparql-DkYu6x3z.js +2 -0
- package/src/ui/dist/assets/spreadsheet-BCZA_wO0.js +2 -0
- package/src/ui/dist/assets/sql-D0XecflT.js +2 -0
- package/src/ui/dist/assets/stateDiagram-2N3HPSRC-Ck48D1ck.js +2 -0
- package/src/ui/dist/assets/stateDiagram-v2-6OUMAXLB-CdfRCJAJ.js +2 -0
- package/src/ui/dist/assets/stex-C3f8Ysf7.js +2 -0
- package/src/ui/dist/assets/style-C3QI7VPf.css +1 -0
- package/src/ui/dist/assets/stylus-B533Al4x.js +2 -0
- package/src/ui/dist/assets/subset-shared.chunk-eAtLHmbi.js +85 -0
- package/src/ui/dist/assets/subset-worker.chunk-DeHCTnTk.js +2 -0
- package/src/ui/dist/assets/sv-SE-XGPEYMSR-BeOYCU-8.js +11 -0
- package/src/ui/dist/assets/swift-BzpIVaGY.js +2 -0
- package/src/ui/dist/assets/swimlanes-5IMT3BWC-DOy22UjP.js +3 -0
- package/src/ui/dist/assets/swimlanesDiagram-G3AALYLV-sicPbVOg.js +9 -0
- package/src/ui/dist/assets/ta-IN-2NMHFXQM-CtS68WDd.js +10 -0
- package/src/ui/dist/assets/tcl-DVfN8rqt.js +2 -0
- package/src/ui/dist/assets/{terminal-BBQvnOAK.js → terminal-DEby5Y_P.js} +2 -2
- package/src/ui/dist/assets/terminalControl-BkFeDVXo.js +134 -0
- package/src/ui/dist/assets/textile-CnDTJFAw.js +2 -0
- package/src/ui/dist/assets/th-TH-HPSO5L25-DCHY9-Co.js +3 -0
- package/src/ui/dist/assets/tiddlywiki-DO-Gjzrf.js +2 -0
- package/src/ui/dist/assets/tiki-DGYXhP31.js +2 -0
- package/src/ui/dist/assets/timeline-definition-FHXFAJF6-DVkG72qO.js +121 -0
- package/src/ui/dist/assets/toml-Bm5Em-hy.js +2 -0
- package/src/ui/dist/assets/tr-TR-DEFEU3FU-y5-vjTv8.js +8 -0
- package/src/ui/dist/assets/troff-wAsdV37c.js +2 -0
- package/src/ui/dist/assets/ttcn-CfJYG6tj.js +2 -0
- package/src/ui/dist/assets/ttcn-cfg-B9xdYoR4.js +2 -0
- package/src/ui/dist/assets/turtle-B1tBg_DP.js +2 -0
- package/src/ui/dist/assets/uk-UA-QMV73CPH-BRbR5It6.js +7 -0
- package/src/ui/dist/assets/useReducedMotion-D07XP52Q.js +12 -0
- package/src/ui/dist/assets/vb-CmGdzxic.js +2 -0
- package/src/ui/dist/assets/vbscript-BuJXcnF6.js +2 -0
- package/src/ui/dist/assets/velocity-D8B20fx6.js +2 -0
- package/src/ui/dist/assets/vennDiagram-L72KCM5P-DwMwBlf-.js +35 -0
- package/src/ui/dist/assets/verilog-C6RDOZhf.js +2 -0
- package/src/ui/dist/assets/vhdl-lSbBsy5d.js +2 -0
- package/src/ui/dist/assets/vi-VN-M7AON7JQ-s6wSBv2s.js +6 -0
- package/src/ui/dist/assets/wardleyDiagram-EHGQE667-CNuuvzEg.js +79 -0
- package/src/ui/dist/assets/webidl-ZXfAyPTL.js +2 -0
- package/src/ui/dist/assets/{x-DoIOGAJz.js → x-B84wq78k.js} +2 -2
- package/src/ui/dist/assets/xquery-DzFWVndE.js +2 -0
- package/src/ui/dist/assets/xychartDiagram-FW5EYKEG-dUpZxmn6.js +8 -0
- package/src/ui/dist/assets/yacas-BJ4BC0dw.js +2 -0
- package/src/ui/dist/assets/z80-Hz9HOZM7.js +2 -0
- package/src/ui/dist/assets/zh-CN-LNUGB5OW-Cu_-POQN.js +11 -0
- package/src/ui/dist/assets/zh-HK-E62DVLB3-BCx4W4qZ.js +2 -0
- package/src/ui/dist/assets/zh-TW-RAJ6MFWO-02wx_Mrj.js +10 -0
- package/src/ui/dist/icons/excalidraw.svg +1 -0
- package/src/ui/dist/icons/markdown.svg +1 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/package.json +10 -0
- package/src/ui/playwright.config.ts +36 -0
- package/src/ui/pnpm-workspace.yaml +2 -0
- package/src/ui/public/icons/excalidraw.svg +1 -0
- package/src/ui/public/icons/markdown.svg +1 -0
- package/src/ui/tsconfig.json +2 -1
- package/src/ui/vite.config.ts +1 -0
- package/src/wf-ui-server/__tests__/a2a-store.test.mjs +432 -15
- package/src/wf-ui-server/__tests__/action-registry-loader.test.mjs +156 -0
- package/src/wf-ui-server/__tests__/claude-stream-json.test.mjs +397 -0
- package/src/wf-ui-server/__tests__/codex-appserver.test.mjs +392 -0
- package/src/wf-ui-server/__tests__/component-node-api.test.mjs +881 -0
- package/src/wf-ui-server/__tests__/component-node-store.test.mjs +401 -0
- package/src/wf-ui-server/__tests__/control-plane-acceptance.test.mjs +2027 -0
- package/src/wf-ui-server/__tests__/control-plane-cli-smoke.test.mjs +1014 -0
- package/src/wf-ui-server/__tests__/dispatch-lifecycle.test.mjs +330 -0
- package/src/wf-ui-server/__tests__/effort-compatibility-red-092.test.mjs +151 -0
- package/src/wf-ui-server/__tests__/graph-layout-agent-tree.test.mjs +254 -0
- package/src/wf-ui-server/__tests__/graph-layout-tidy.test.mjs +260 -0
- package/src/wf-ui-server/__tests__/graph-layout.test.mjs +84 -0
- package/src/wf-ui-server/__tests__/node-config-api.test.mjs +384 -0
- package/src/wf-ui-server/__tests__/opencode-server.test.mjs +526 -0
- package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +2 -2
- package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +71 -1
- package/src/wf-ui-server/__tests__/runtime-config.test.mjs +3 -3
- package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +21 -0
- package/src/wf-ui-server/__tests__/runtime-review.test.mjs +828 -0
- package/src/wf-ui-server/__tests__/server.integration.test.mjs +1463 -11
- package/src/wf-ui-server/__tests__/session-cleanup.test.mjs +159 -0
- package/src/wf-ui-server/__tests__/session-registry.test.mjs +52 -3
- package/src/wf-ui-server/__tests__/settings.test.mjs +12 -2
- package/src/wf-ui-server/__tests__/spawn-gate.test.mjs +78 -0
- package/src/wf-ui-server/__tests__/task-group-parser.test.mjs +120 -0
- package/src/wf-ui-server/__tests__/task-group-tag.test.mjs +264 -0
- package/src/wf-ui-server/__tests__/task-parser.test.mjs +49 -3
- package/src/wf-ui-server/__tests__/terminal-context-hardening.test.mjs +122 -0
- package/src/wf-ui-server/__tests__/terminal-context.test.mjs +164 -0
- package/src/wf-ui-server/__tests__/terminal-store.test.mjs +4 -4
- package/src/wf-ui-server/__tests__/wf-browser-api.test.mjs +599 -0
- package/src/wf-ui-server/__tests__/wf-browser-store.test.mjs +184 -0
- package/src/wf-ui-server/__tests__/workflow-action-registry.test.mjs +282 -0
- package/src/wf-ui-server/__tests__/workflow-agent-communication.test.mjs +411 -0
- package/src/wf-ui-server/__tests__/workflow-agent-context.test.mjs +323 -0
- package/src/wf-ui-server/__tests__/workflow-agent-cooperation-permissions.test.mjs +318 -0
- package/src/wf-ui-server/__tests__/workflow-agent-messages-structured.test.mjs +458 -0
- package/src/wf-ui-server/__tests__/workflow-agent-routing.test.mjs +434 -0
- package/src/wf-ui-server/__tests__/workflow-agent-runtime.test.mjs +324 -0
- package/src/wf-ui-server/__tests__/workflow-api-integration.test.mjs +263 -0
- package/src/wf-ui-server/__tests__/workflow-api-phase3.test.mjs +1213 -0
- package/src/wf-ui-server/__tests__/workflow-claude-materialize.test.mjs +209 -0
- package/src/wf-ui-server/__tests__/workflow-cli-registry-manuals.test.mjs +173 -0
- package/src/wf-ui-server/__tests__/workflow-cli-surface.test.mjs +176 -0
- package/src/wf-ui-server/__tests__/workflow-codex-resume-id.test.mjs +556 -0
- package/src/wf-ui-server/__tests__/workflow-cooperation-enforcement.test.mjs +553 -0
- package/src/wf-ui-server/__tests__/workflow-depth-resume-dock.test.mjs +846 -0
- package/src/wf-ui-server/__tests__/workflow-display-node.test.mjs +150 -0
- package/src/wf-ui-server/__tests__/workflow-edge-update.test.mjs +296 -0
- package/src/wf-ui-server/__tests__/workflow-file-cache-lock-watch.test.mjs +380 -0
- package/src/wf-ui-server/__tests__/workflow-file-formats.test.mjs +994 -0
- package/src/wf-ui-server/__tests__/workflow-file-node-actions.test.mjs +294 -0
- package/src/wf-ui-server/__tests__/workflow-goal-actions.test.mjs +357 -0
- package/src/wf-ui-server/__tests__/workflow-goal-task-autoclose.test.mjs +256 -0
- package/src/wf-ui-server/__tests__/workflow-graceful-stop.test.mjs +108 -0
- package/src/wf-ui-server/__tests__/workflow-graph-undo.test.mjs +524 -0
- package/src/wf-ui-server/__tests__/workflow-layout.test.mjs +732 -0
- package/src/wf-ui-server/__tests__/workflow-magnetic-topology.test.mjs +222 -0
- package/src/wf-ui-server/__tests__/workflow-markdown-lock.test.mjs +359 -0
- package/src/wf-ui-server/__tests__/workflow-mcp-hub.test.mjs +174 -0
- package/src/wf-ui-server/__tests__/workflow-node-manual-schema.test.mjs +241 -0
- package/src/wf-ui-server/__tests__/workflow-node-runtime.test.mjs +1991 -0
- package/src/wf-ui-server/__tests__/workflow-node-store-utils.test.mjs +125 -0
- package/src/wf-ui-server/__tests__/workflow-registry-drift.test.mjs +296 -0
- package/src/wf-ui-server/__tests__/workflow-resource-handle-four-side.test.mjs +217 -0
- package/src/wf-ui-server/__tests__/workflow-role-profile.test.mjs +234 -0
- package/src/wf-ui-server/__tests__/workflow-set-model.test.mjs +261 -0
- package/src/wf-ui-server/__tests__/workflow-skill-group-context-enabled.test.mjs +131 -0
- package/src/wf-ui-server/__tests__/workflow-skill-group-enable-disable.test.mjs +186 -0
- package/src/wf-ui-server/__tests__/workflow-skills-hub.test.mjs +125 -0
- package/src/wf-ui-server/__tests__/workflow-state-reconcile.test.mjs +139 -0
- package/src/wf-ui-server/__tests__/workflow-subagent-strategy-matrix.test.mjs +529 -0
- package/src/wf-ui-server/__tests__/workflow-team-flow.test.mjs +278 -0
- package/src/wf-ui-server/__tests__/workflow-terminal-control-live.test.mjs +294 -0
- package/src/wf-ui-server/__tests__/workflow-terminal-control.test.mjs +631 -0
- package/src/wf-ui-server/__tests__/workflow-timer-wakeup.test.mjs +318 -0
- package/src/wf-ui-server/__tests__/workflow-unified-resume.test.mjs +537 -0
- package/src/wf-ui-server/__tests__/workspace-api-hardening.test.mjs +187 -0
- package/src/wf-ui-server/__tests__/workspace-api.test.mjs +382 -0
- package/src/wf-ui-server/__tests__/workspace-reveal.test.mjs +138 -0
- package/src/wf-ui-server/__tests__/workspace-store-hardening.test.mjs +255 -0
- package/src/wf-ui-server/__tests__/workspace-store.test.mjs +208 -0
- package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +52 -25
- package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +12 -6
- package/src/wf-ui-server/__tests__/ws-wf-browser.integration.test.mjs +488 -0
- package/src/wf-ui-server/a2a-store.mjs +3007 -89
- package/src/wf-ui-server/action-registry.mjs +277 -0
- package/src/wf-ui-server/bridge-store.mjs +276 -0
- package/src/wf-ui-server/chat-driver.mjs +220 -0
- package/src/wf-ui-server/chat-store.mjs +109 -0
- package/src/wf-ui-server/claude-transcript-materializer.mjs +235 -0
- package/src/wf-ui-server/codex-update-prompt.mjs +53 -0
- package/src/wf-ui-server/component-node-store.mjs +689 -0
- package/src/wf-ui-server/composition-api.mjs +436 -0
- package/src/wf-ui-server/control-plane-token.mjs +17 -0
- package/src/wf-ui-server/dispatch-contract.mjs +150 -0
- package/src/wf-ui-server/drivers/claude-stream-json.mjs +371 -0
- package/src/wf-ui-server/drivers/codex-appserver.mjs +513 -0
- package/src/wf-ui-server/drivers/opencode-server.mjs +486 -0
- package/src/wf-ui-server/excalidraw-svg.mjs +141 -0
- package/src/wf-ui-server/file-format-adapters.mjs +511 -0
- package/src/wf-ui-server/file-watcher.mjs +215 -0
- package/src/wf-ui-server/graph-layout.mjs +778 -0
- package/src/wf-ui-server/harness-env.mjs +46 -0
- package/src/wf-ui-server/json-store-utils.mjs +48 -0
- package/src/wf-ui-server/model-capability.mjs +773 -0
- package/src/wf-ui-server/node-config-store.mjs +233 -0
- package/src/wf-ui-server/node-store-utils.mjs +88 -0
- package/src/wf-ui-server/peer-capsule.mjs +56 -1
- package/src/wf-ui-server/pty-adapter.mjs +513 -11
- package/src/wf-ui-server/reveal-foreground.mjs +69 -0
- package/src/wf-ui-server/runtime-detector.mjs +52 -6
- package/src/wf-ui-server/server.mjs +5613 -403
- package/src/wf-ui-server/session-cleanup.mjs +363 -0
- package/src/wf-ui-server/session-registry.mjs +179 -3
- package/src/wf-ui-server/settings.mjs +25 -0
- package/src/wf-ui-server/spawn-gate.mjs +46 -0
- package/src/wf-ui-server/task-parser.mjs +88 -3
- package/src/wf-ui-server/terminal-store.mjs +225 -11
- package/src/wf-ui-server/timer-wakeup-scheduler.mjs +226 -0
- package/src/wf-ui-server/wf-browser-store.mjs +733 -0
- package/src/wf-ui-server/workflow-agent-context.mjs +1731 -0
- package/src/wf-ui-server/workflow-capability-node-store.mjs +676 -0
- package/src/wf-ui-server/workflow-event-node-store.mjs +623 -0
- package/src/wf-ui-server/workflow-goal-node-store.mjs +328 -0
- package/src/wf-ui-server/workflow-graph-store.mjs +623 -0
- package/src/wf-ui-server/workflow-mcp-hub.mjs +286 -0
- package/src/wf-ui-server/workflow-node-runtime.mjs +1767 -0
- package/src/wf-ui-server/workflow-node-settings-store.mjs +339 -0
- package/src/wf-ui-server/workflow-node-types/agent-node.mjs +920 -0
- package/src/wf-ui-server/workflow-node-types/display-node.mjs +151 -0
- package/src/wf-ui-server/workflow-node-types/excalidraw-node.mjs +145 -0
- package/src/wf-ui-server/workflow-node-types/file-node.mjs +484 -0
- package/src/wf-ui-server/workflow-node-types/github-trigger-node.mjs +207 -0
- package/src/wf-ui-server/workflow-node-types/goal-node.mjs +491 -0
- package/src/wf-ui-server/workflow-node-types/markdown-node.mjs +225 -0
- package/src/wf-ui-server/workflow-node-types/mcp-connector-node.mjs +71 -0
- package/src/wf-ui-server/workflow-node-types/role-profile-store.mjs +191 -0
- package/src/wf-ui-server/workflow-node-types/skill-group-node.mjs +98 -0
- package/src/wf-ui-server/workflow-node-types/timer-node.mjs +607 -0
- package/src/wf-ui-server/workflow-ontology.mjs +681 -0
- package/src/wf-ui-server/workflow-operation-store.mjs +115 -0
- package/src/wf-ui-server/workflow-skills-hub.mjs +366 -0
- package/src/wf-ui-server/workflow-skills-market.mjs +723 -0
- package/src/wf-ui-server/workspace-store.mjs +951 -0
- package/src/wf-ui-server/ws-chat.mjs +299 -0
- package/src/wf-ui-server/ws-events.mjs +45 -20
- package/src/wf-ui-server/ws-terminal.mjs +165 -22
- package/src/wf-ui-server/ws-wf-browser.mjs +419 -0
- package/templates/common/.claude/agents/review-manager.md +33 -29
- package/templates/common/.claude/commands/wf-browser.md +1 -1
- package/templates/common/.claude/commands/wf-help.md +5 -3
- package/templates/common/.claude/commands/wf-init.md +44 -0
- package/templates/common/.claude/commands/wf-learn.md +34 -8
- package/templates/common/.claude/commands/wf-max.md +19 -9
- package/templates/common/.claude/commands/wf-review.md +3 -3
- package/templates/common/.claude/commands/wf-search.md +55 -0
- package/templates/common/.claude/commands/wf-task-archive.md +5 -4
- package/templates/common/.claude/commands/wf-task-list.md +6 -3
- package/templates/common/.claude/commands/wf-task-record.md +10 -2
- package/templates/common/.claude/commands/wf-ui.md +12 -5
- package/templates/common/.claude/commands/wf-update.md +4 -0
- package/templates/common/.claude/commands/wf.md +12 -1
- package/templates/common/.claude/rules/ecc/common.md +10 -1
- package/templates/common/.claude/skills/wf/SKILL.md +36 -0
- package/templates/common/.claude/skills/wf-browser/SKILL.md +427 -122
- package/templates/common/.claude/skills/wf-browser/references/BROWSER_CONTROL_PATTERNS.md +43 -0
- package/templates/common/.claude/skills/wf-init/SKILL.md +33 -0
- package/templates/common/.claude/skills/wf-learn/SKILL.md +106 -26
- package/templates/common/.claude/skills/wf-max/SKILL.md +45 -11
- package/templates/common/.claude/skills/wf-review/SKILL.md +26 -54
- package/templates/common/.claude/skills/wf-search/SKILL.md +122 -0
- package/templates/common/.claude/skills/wf-search/references/evidence-contract.md +77 -0
- package/templates/common/.claude/skills/wf-search/references/policies/compare.md +38 -0
- package/templates/common/.claude/skills/wf-search/references/policies/fact.md +39 -0
- package/templates/common/.claude/skills/wf-search/references/policies/troubleshoot.md +39 -0
- package/templates/common/.claude/skills/wf-search/references/policies/verify.md +44 -0
- package/templates/common/.claude/skills/wf-search/references/source-routing.md +40 -0
- package/templates/common/.claude/skills/wf-search/references/tool-adapters.md +38 -0
- package/templates/common/.claude/skills/wf-ui/SKILL.md +146 -12
- package/templates/common/.claude/skills/wf-update/SKILL.md +5 -1
- package/templates/common/.harness-version +188 -85
- package/templates/common/.opencode/agents/review-manager.md +31 -27
- package/templates/common/.opencode/commands/wf-browser.md +1 -1
- package/templates/common/.opencode/commands/wf-help.md +5 -3
- package/templates/common/.opencode/commands/wf-init.md +44 -0
- package/templates/common/.opencode/commands/wf-learn.md +34 -8
- package/templates/common/.opencode/commands/wf-max.md +19 -9
- package/templates/common/.opencode/commands/wf-review.md +3 -3
- package/templates/common/.opencode/commands/wf-search.md +55 -0
- package/templates/common/.opencode/commands/wf-task-archive.md +5 -7
- package/templates/common/.opencode/commands/wf-task-list.md +6 -6
- package/templates/common/.opencode/commands/wf-task-record.md +10 -5
- package/templates/common/.opencode/commands/wf-ui.md +12 -5
- package/templates/common/.opencode/commands/wf-update.md +4 -0
- package/templates/common/.opencode/commands/wf.md +12 -1
- package/templates/common/CLAUDE.md +61 -3
- package/templates/common/Harness/MEMORY.md +8 -5
- package/templates/common/Harness/README.md +14 -10
- package/templates/common/Harness/a2a/action-registry.json +4397 -0
- package/templates/common/Harness/a2a/role-graph.json +16 -3
- package/templates/common/Harness/a2a/skills/terminal-control.json +45 -1
- package/templates/common/Harness/a2a/skills/wf-ui-map.json +50 -0
- package/templates/common/Harness/a2a/skills/workflow-agent-node.json +143 -0
- package/templates/common/Harness/a2a/skills/workflow-context.json +95 -0
- package/templates/common/Harness/a2a/skills/workflow-diagram-node.json +60 -0
- package/templates/common/Harness/a2a/skills/workflow-display-node.json +69 -0
- package/templates/common/Harness/a2a/skills/workflow-file-node.json +92 -0
- package/templates/common/Harness/a2a/skills/workflow-goal-node.json +76 -0
- package/templates/common/Harness/a2a/skills/workflow-markdown-node.json +79 -0
- package/templates/common/Harness/a2a/skills/workflow-mcp-connector-node.json +59 -0
- package/templates/common/Harness/a2a/skills/workflow-node-actions.json +105 -0
- package/templates/common/Harness/a2a/skills/workflow-node-map.json +71 -0
- package/templates/common/Harness/a2a/skills/workflow-ontology.json +54 -0
- package/templates/common/Harness/a2a/skills/workflow-resource-node.json +68 -0
- package/templates/common/Harness/a2a/skills/workflow-skill-group-node.json +56 -0
- package/templates/common/Harness/a2a/skills/workflow-timer-node.json +81 -0
- package/templates/common/Harness/a2a/workflows.json +33 -0
- package/templates/common/Harness/ownership.manifest.json +263 -2
- package/templates/common/Harness/project/architecture.md +499 -0
- package/templates/common/Harness/research/README.md +20 -0
- package/templates/common/Harness/scripts/README.md +70 -6
- package/templates/common/Harness/scripts/a2a-terminal.mjs +12 -4
- package/templates/common/Harness/scripts/context-budget.mjs +7 -3
- package/templates/common/Harness/scripts/lib/memory-context.mjs +400 -0
- package/templates/common/Harness/scripts/lib/path-scope.mjs +77 -0
- package/templates/common/Harness/scripts/lib/research-policy.mjs +99 -0
- package/templates/common/Harness/scripts/lib/wf-search.mjs +189 -0
- package/templates/common/Harness/scripts/memory-context.mjs +63 -0
- package/templates/common/Harness/scripts/research-policy.mjs +27 -0
- package/templates/common/Harness/scripts/task-context.mjs +547 -0
- package/templates/common/Harness/scripts/task-group-index.mjs +242 -0
- package/templates/common/Harness/scripts/task-state.mjs +659 -95
- package/templates/common/Harness/scripts/validate-harness.mjs +70 -28
- package/templates/common/Harness/scripts/wf-auto-update-prompt.mjs +24 -0
- package/templates/common/Harness/scripts/wf-remove.mjs +10 -0
- package/templates/common/Harness/scripts/wf-search.mjs +10 -0
- package/templates/common/Harness/scripts/wf-ui-control.mjs +3144 -0
- package/templates/common/Harness/scripts/wf-update-check.mjs +274 -51
- package/templates/common/Harness/scripts/wf-update-runner.mjs +53 -16
- package/templates/common/Harness/settings.json +10 -0
- package/templates/common/Harness/specs/guides/SETUP.md +4 -3
- package/templates/common/Harness/specs/protocols/HARNESS_BRIDGE.md +429 -48
- package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +24 -0
- package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +14 -11
- package/templates/common/Harness/specs/runtime/command-surface.json +28 -0
- package/templates/common/Harness/specs/runtime/context-loading.md +6 -0
- package/templates/common/Harness/specs/runtime/dispatch.md +9 -1
- package/templates/common/Harness/specs/runtime/subagents.md +20 -5
- package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +19 -0
- package/templates/common/Harness/specs/workflows/WF-AUTO.md +21 -0
- package/templates/common/Harness/specs/workflows/WF-KERNEL.md +4 -2
- package/templates/common/Harness/specs/workflows/WF-MAX.md +39 -12
- package/templates/common/Harness/specs/workflows/WF-STATE.md +62 -14
- package/templates/common/Harness/specs/workflows/WF.md +32 -0
- package/templates/common/Harness/tasks/INDEX.json +7 -0
- package/templates/common/Harness/tasks/INDEX.md +12 -0
- package/templates/common/Harness/tasks/_template/PLAN.md +6 -0
- package/templates/common/Harness/tasks/_template/PROGRESS.md +5 -0
- package/templates/common/Harness/tasks/_template/STATE.json +22 -0
- package/templates/common/Harness/templates/TEST_PLAN.template.md +5 -4
- package/templates/common/Harness/templates/UI_CONTRACT.template.md +7 -5
- package/templates/common/Harness/wf-browser/README.md +69 -0
- package/templates/common/README.md +17 -0
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +0 -17
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +0 -1
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +0 -7
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +0 -1
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +0 -12
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +0 -1
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js +0 -50
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +0 -1
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +0 -74
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +0 -1
- package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +0 -1
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +0 -39
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +0 -1
- package/src/ui/dist/assets/index-BOVYWntB.js +0 -117
- package/src/ui/dist/assets/index-BOVYWntB.js.map +0 -1
- package/src/ui/dist/assets/index-DaUObq0H.css +0 -1
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +0 -7
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +0 -1
- package/src/ui/dist/assets/plus-QuTJyoT3.js +0 -7
- package/src/ui/dist/assets/plus-QuTJyoT3.js.map +0 -1
- package/src/ui/dist/assets/proxy-D0vUbds5.js.map +0 -1
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +0 -1
- package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +0 -1
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +0 -7
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +0 -1
- package/src/ui/dist/assets/x-DoIOGAJz.js.map +0 -1
- package/src/ui/pnpm-lock.yaml +0 -1582
- package/src/ui/src/App.tsx +0 -86
- package/src/ui/src/api.ts +0 -93
- package/src/ui/src/components/AgentsRoute.tsx +0 -503
- package/src/ui/src/components/Footer.tsx +0 -69
- package/src/ui/src/components/Header.tsx +0 -175
- package/src/ui/src/components/LoadingView.tsx +0 -22
- package/src/ui/src/components/RolesRoute.tsx +0 -169
- package/src/ui/src/components/SettingsRoute.tsx +0 -166
- package/src/ui/src/components/TaskList.tsx +0 -388
- package/src/ui/src/components/TerminalDrawer.tsx +0 -611
- package/src/ui/src/components/WorkflowRoute.tsx +0 -670
- package/src/ui/src/hooks/useReducedMotion.ts +0 -17
- package/src/ui/src/hooks/useServerConnection.ts +0 -114
- package/src/ui/src/index.css +0 -281
- package/src/ui/src/main.tsx +0 -11
- package/src/ui/src/types.ts +0 -108
- /package/src/ui/dist/assets/{TerminalDrawer-6GBZ9nXN.css → terminalControl-6GBZ9nXN.css} +0 -0
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import zlib from 'node:zlib';
|
|
6
|
+
import { makeHarnessTempRoot } from '../../../tests/support/temp-root.js';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// task-upgrade-file-node — acceptance tests, wave W2 (implemented actions).
|
|
10
|
+
//
|
|
11
|
+
// AC mapping (task PLAN "验收标准"):
|
|
12
|
+
// AC-5 Excel: file.readXlsx returns the sheet-name list;
|
|
13
|
+
// file.readXlsxSheet(sheet, page, pageSize) returns headers + the
|
|
14
|
+
// requested page of rows (paged, never whole-sheet parse); a corrupt
|
|
15
|
+
// file produces a clear machine-readable error.
|
|
16
|
+
// AC-6 PDF: file.readPdf returns total page count + metadata;
|
|
17
|
+
// file.readPdfPage({page, pageCount}) returns page-range text with
|
|
18
|
+
// the isEvalSupported:false security baseline; corrupt PDFs fail
|
|
19
|
+
// with a clear error.
|
|
20
|
+
// AC-7 ZIP: file.readZipEntries returns the entry list (name, size,
|
|
21
|
+
// compressedSize, directory flag, UTF-8 flag; GBK names decoded —
|
|
22
|
+
// no whole-archive read); file.readZipEntry previews a single text
|
|
23
|
+
// entry (maxBytes bound); file.extractZipEntry extracts a single
|
|
24
|
+
// entry to a workspace-controlled destDir and rejects Zip Slip
|
|
25
|
+
// traversal entries.
|
|
26
|
+
//
|
|
27
|
+
// Fixture strategy (PLAN D4):
|
|
28
|
+
// - ZIP fixtures are REAL, generated in-test by the dependency-free builder
|
|
29
|
+
// below (zlib.deflateRawSync + table CRC32). They exercise entry sizes,
|
|
30
|
+
// compression, directory flags, UTF-8 names and a GBK-named entry (GBK
|
|
31
|
+
// bytes for 中文名.txt = d6d0 cec4 c3fb 2e 747874).
|
|
32
|
+
// - XLSX fixture is REAL: an .xlsx is a zip archive, so the same builder
|
|
33
|
+
// emits the 5 required parts (Content_Types, package rels, workbook,
|
|
34
|
+
// workbook rels, one worksheet with inline strings). read-excel-file
|
|
35
|
+
// parses it (verified during authoring: sheets ['Data'], header
|
|
36
|
+
// ['Name','Score'], 4 data rows).
|
|
37
|
+
// - PDF fixture is REAL: a minimal 2-page PDF built by hand with exact xref
|
|
38
|
+
// offsets (objects are assembled first, offsets computed from the byte
|
|
39
|
+
// lengths, trailer startxref points at the computed xref position) plus
|
|
40
|
+
// an /Info dict carrying Title/Author/Subject/Producer. unpdf
|
|
41
|
+
// (getDocumentProxy) parses it (verified during authoring: numPages 2,
|
|
42
|
+
// text ['Hello PDF', 'Second Page']).
|
|
43
|
+
// - The corrupt-input tests use deliberately broken bytes that are NOT
|
|
44
|
+
// valid xlsx/zip/pdf and assert the machine-readable error contract
|
|
45
|
+
// (err.code + 4xx statusCode) — never the message.
|
|
46
|
+
//
|
|
47
|
+
// Error contract for corrupt inputs:
|
|
48
|
+
// Corrupt/malformed files THROW (ComponentNodeError) with a
|
|
49
|
+
// machine-readable err.code matching
|
|
50
|
+
// /XLSX|PARSE|INVALID|CORRUPT|UNSUPPORTED/i (Excel) and
|
|
51
|
+
// /PDF|PARSE|INVALID|CORRUPT|UNSUPPORTED/i (PDF), statusCode 4xx —
|
|
52
|
+
// consistent with the ComponentNodeError precedent (markdown_locked, 409).
|
|
53
|
+
// Tests match err.code ONLY (never the message).
|
|
54
|
+
//
|
|
55
|
+
// Async contract: the parsing libraries (read-excel-file / unpdf / yauzl)
|
|
56
|
+
// are all promise/callback based, so every file.* format action is async and
|
|
57
|
+
// the tests await them (assert.rejects for the corrupt-input cases). All
|
|
58
|
+
// assertions from the original RED tests are preserved; only concrete values
|
|
59
|
+
// were tightened to the real fixtures (sheet name, page count, header row).
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
// ---- Dependency-free minimal ZIP builder ---------------------------------
|
|
63
|
+
// Local file header + central directory + EOCD, entries deflated with
|
|
64
|
+
// zlib.deflateRawSync. Enough for yauzl-compatible archives.
|
|
65
|
+
|
|
66
|
+
let crcTable = null;
|
|
67
|
+
function crc32(buffer) {
|
|
68
|
+
if (!crcTable) {
|
|
69
|
+
crcTable = new Int32Array(256);
|
|
70
|
+
for (let n = 0; n < 256; n += 1) {
|
|
71
|
+
let c = n;
|
|
72
|
+
for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
73
|
+
crcTable[n] = c;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
let crc = -1;
|
|
77
|
+
for (let i = 0; i < buffer.length; i += 1) crc = (crc >>> 8) ^ crcTable[(crc ^ buffer[i]) & 0xff];
|
|
78
|
+
return (crc ^ -1) >>> 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// entries: [{ name, content?, nameBytes?, utf8?, directory?, usize? }]
|
|
82
|
+
// name display name (utf8-encoded unless nameBytes given)
|
|
83
|
+
// nameBytes raw name bytes (for GBK names); utf8:false leaves the UTF-8
|
|
84
|
+
// general-purpose flag unset
|
|
85
|
+
// content string or Buffer; absent for directory entries
|
|
86
|
+
// usize OPTIONAL uncompressed-size override written to both the local
|
|
87
|
+
// header and the central directory (W4F-M1 test seam: lets a test
|
|
88
|
+
// fabricate a lying central-directory size WITHOUT real payload)
|
|
89
|
+
function buildZip(entries) {
|
|
90
|
+
const localChunks = [];
|
|
91
|
+
const centralRecords = [];
|
|
92
|
+
let offset = 0;
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const nameBytes = entry.nameBytes || Buffer.from(entry.name, 'utf8');
|
|
95
|
+
const content = entry.content == null
|
|
96
|
+
? Buffer.alloc(0)
|
|
97
|
+
: (Buffer.isBuffer(entry.content) ? entry.content : Buffer.from(entry.content, 'utf8'));
|
|
98
|
+
const usize = Number.isInteger(entry.usize) ? entry.usize : content.length;
|
|
99
|
+
const flags = entry.utf8 === false ? 0 : 0x0800;
|
|
100
|
+
const crc = crc32(content);
|
|
101
|
+
const deflated = content.length === 0 ? Buffer.alloc(0) : zlib.deflateRawSync(content);
|
|
102
|
+
const method = content.length === 0 ? 0 : 8;
|
|
103
|
+
|
|
104
|
+
const local = Buffer.alloc(30);
|
|
105
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
106
|
+
local.writeUInt16LE(20, 4); // version needed
|
|
107
|
+
local.writeUInt16LE(flags, 6);
|
|
108
|
+
local.writeUInt16LE(method, 8);
|
|
109
|
+
local.writeUInt16LE(0, 10); // mod time
|
|
110
|
+
local.writeUInt16LE(0x21, 12); // mod date 1980-01-01
|
|
111
|
+
local.writeUInt32LE(crc, 14);
|
|
112
|
+
local.writeUInt32LE(deflated.length, 18);
|
|
113
|
+
local.writeUInt32LE(usize, 22);
|
|
114
|
+
local.writeUInt16LE(nameBytes.length, 26);
|
|
115
|
+
local.writeUInt16LE(0, 28); // extra length
|
|
116
|
+
localChunks.push(local, nameBytes, deflated);
|
|
117
|
+
|
|
118
|
+
centralRecords.push({
|
|
119
|
+
nameBytes,
|
|
120
|
+
flags,
|
|
121
|
+
method,
|
|
122
|
+
crc,
|
|
123
|
+
csize: deflated.length,
|
|
124
|
+
usize,
|
|
125
|
+
offset,
|
|
126
|
+
isDir: Boolean(entry.directory),
|
|
127
|
+
});
|
|
128
|
+
offset += 30 + nameBytes.length + deflated.length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const cdChunks = [];
|
|
132
|
+
for (const rec of centralRecords) {
|
|
133
|
+
const cd = Buffer.alloc(46);
|
|
134
|
+
cd.writeUInt32LE(0x02014b50, 0);
|
|
135
|
+
cd.writeUInt16LE(20, 4); // version made by
|
|
136
|
+
cd.writeUInt16LE(20, 6); // version needed
|
|
137
|
+
cd.writeUInt16LE(rec.flags, 8);
|
|
138
|
+
cd.writeUInt16LE(rec.method, 10);
|
|
139
|
+
cd.writeUInt16LE(0, 12); // mod time
|
|
140
|
+
cd.writeUInt16LE(0x21, 14); // mod date
|
|
141
|
+
cd.writeUInt32LE(rec.crc, 16);
|
|
142
|
+
cd.writeUInt32LE(rec.csize, 20);
|
|
143
|
+
cd.writeUInt32LE(rec.usize, 24);
|
|
144
|
+
cd.writeUInt16LE(rec.nameBytes.length, 28);
|
|
145
|
+
cd.writeUInt16LE(0, 30); // extra length
|
|
146
|
+
cd.writeUInt16LE(0, 32); // comment length
|
|
147
|
+
cd.writeUInt16LE(0, 34); // disk number
|
|
148
|
+
cd.writeUInt16LE(0, 36); // internal attrs
|
|
149
|
+
cd.writeUInt32LE(rec.isDir ? 0x10 : 0, 38);
|
|
150
|
+
cd.writeUInt32LE(rec.offset, 42);
|
|
151
|
+
cdChunks.push(cd, rec.nameBytes);
|
|
152
|
+
}
|
|
153
|
+
const centralBuf = Buffer.concat(cdChunks);
|
|
154
|
+
|
|
155
|
+
const eocd = Buffer.alloc(22);
|
|
156
|
+
eocd.writeUInt32LE(0x06054b50, 0);
|
|
157
|
+
eocd.writeUInt16LE(0, 4);
|
|
158
|
+
eocd.writeUInt16LE(0, 6);
|
|
159
|
+
eocd.writeUInt16LE(centralRecords.length, 8);
|
|
160
|
+
eocd.writeUInt16LE(centralRecords.length, 10);
|
|
161
|
+
eocd.writeUInt32LE(centralBuf.length, 12);
|
|
162
|
+
eocd.writeUInt32LE(offset, 16);
|
|
163
|
+
eocd.writeUInt16LE(0, 20);
|
|
164
|
+
return Buffer.concat([...localChunks, centralBuf, eocd]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// GBK bytes for 中文名.txt (d6d0 cec4 c3fb 2e 747874). Node cannot encode
|
|
168
|
+
// GBK natively and tests must not depend on iconv-lite; this fixed constant
|
|
169
|
+
// is the GBK payload the adapter decodes (iconv-lite, PLAN D4).
|
|
170
|
+
const GBK_NAME_BYTES = Buffer.from('d6d0cec4c3fb2e747874', 'hex');
|
|
171
|
+
|
|
172
|
+
// ---- REAL minimal .xlsx fixture -------------------------------------------
|
|
173
|
+
// An .xlsx is a zip archive; the same builder emits the parts
|
|
174
|
+
// read-excel-file requires (PLAN D4): workbook + workbook rels + one
|
|
175
|
+
// worksheet with inline strings. Parsed during authoring to:
|
|
176
|
+
// sheets: ['Data']
|
|
177
|
+
// sheet 'Data': [['Name','Score'], ['Alice',42], ['Bob',17], ['Carol',88], ['Dave',5]]
|
|
178
|
+
function buildMinimalXlsx() {
|
|
179
|
+
const workbookXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
180
|
+
+ '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"'
|
|
181
|
+
+ ' xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
|
|
182
|
+
+ '<sheets><sheet name="Data" sheetId="1" r:id="rId1"/></sheets></workbook>';
|
|
183
|
+
const relsXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
184
|
+
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
185
|
+
+ '<Relationship Id="rId1"'
|
|
186
|
+
+ ' Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"'
|
|
187
|
+
+ ' Target="worksheets/sheet1.xml"/>'
|
|
188
|
+
+ '</Relationships>';
|
|
189
|
+
const cell = (ref, text) => `<c r="${ref}" t="inlineStr"><is><t>${text}</t></is></c>`;
|
|
190
|
+
const num = (ref, value) => `<c r="${ref}"><v>${value}</v></c>`;
|
|
191
|
+
const sheetXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
192
|
+
+ '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>'
|
|
193
|
+
+ `<row r="1">${cell('A1', 'Name')}${cell('B1', 'Score')}</row>`
|
|
194
|
+
+ `<row r="2">${cell('A2', 'Alice')}${num('B2', 42)}</row>`
|
|
195
|
+
+ `<row r="3">${cell('A3', 'Bob')}${num('B3', 17)}</row>`
|
|
196
|
+
+ `<row r="4">${cell('A4', 'Carol')}${num('B4', 88)}</row>`
|
|
197
|
+
+ `<row r="5">${cell('A5', 'Dave')}${num('B5', 5)}</row>`
|
|
198
|
+
+ '</sheetData></worksheet>';
|
|
199
|
+
return buildZip([
|
|
200
|
+
{
|
|
201
|
+
name: '[Content_Types].xml',
|
|
202
|
+
content: '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
203
|
+
+ '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
|
|
204
|
+
+ '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
|
|
205
|
+
+ '<Default Extension="xml" ContentType="application/xml"/>'
|
|
206
|
+
+ '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
|
|
207
|
+
+ '<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
|
|
208
|
+
+ '</Types>',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: '_rels/.rels',
|
|
212
|
+
content: '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
213
|
+
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
|
214
|
+
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
|
|
215
|
+
+ '</Relationships>',
|
|
216
|
+
},
|
|
217
|
+
{ name: 'xl/workbook.xml', content: workbookXml },
|
|
218
|
+
{ name: 'xl/_rels/workbook.xml.rels', content: relsXml },
|
|
219
|
+
{ name: 'xl/worksheets/sheet1.xml', content: sheetXml },
|
|
220
|
+
]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---- REAL minimal 2-page PDF fixture --------------------------------------
|
|
224
|
+
// Hand-built PDF with exact xref offsets: objects are assembled first, byte
|
|
225
|
+
// offsets are computed from the emitted text, the xref table and trailer
|
|
226
|
+
// startxref point at those offsets. Page 1 "Hello PDF", page 2 "Second
|
|
227
|
+
// Page"; an /Info dict carries Title/Author/Subject/Producer. Verified during
|
|
228
|
+
// authoring with unpdf: numPages 2, text ['Hello PDF', 'Second Page'],
|
|
229
|
+
// meta.info.Title === 'Fixture Report'.
|
|
230
|
+
function buildMinimalPdf() {
|
|
231
|
+
const pages = ['Hello PDF', 'Second Page'];
|
|
232
|
+
const objects = ['<< /Type /Catalog /Pages 2 0 R >>'];
|
|
233
|
+
const kids = pages.map((unused, i) => `${3 + i * 3} 0 R`).join(' ');
|
|
234
|
+
objects.push(`<< /Type /Pages /Kids [${kids}] /Count ${pages.length} >>`);
|
|
235
|
+
for (let i = 0; i < pages.length; i += 1) {
|
|
236
|
+
objects.push(
|
|
237
|
+
`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents ${4 + i * 3} 0 R`
|
|
238
|
+
+ ` /Resources << /Font << /F1 ${5 + i * 3} 0 R >> >> >>`,
|
|
239
|
+
);
|
|
240
|
+
const stream = `BT /F1 12 Tf 72 720 Td (${pages[i]}) Tj ET`;
|
|
241
|
+
objects.push(`<< /Length ${Buffer.byteLength(stream)} >>\nstream\n${stream}\nendstream`);
|
|
242
|
+
objects.push('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>');
|
|
243
|
+
}
|
|
244
|
+
const info = '/Title (Fixture Report) /Author (Test Suite) /Subject (Minimal PDF) /Producer (node test builder)';
|
|
245
|
+
objects.push(`<< ${info} >>`);
|
|
246
|
+
|
|
247
|
+
let out = '%PDF-1.4\n';
|
|
248
|
+
const offsets = [];
|
|
249
|
+
objects.forEach((body, i) => {
|
|
250
|
+
offsets.push(Buffer.byteLength(out, 'utf8'));
|
|
251
|
+
out += `${i + 1} 0 obj\n${body}\nendobj\n`;
|
|
252
|
+
});
|
|
253
|
+
const xrefStart = Buffer.byteLength(out, 'utf8');
|
|
254
|
+
out += `xref\n0 ${objects.length + 1}\n`;
|
|
255
|
+
out += '0000000000 65535 f \n';
|
|
256
|
+
for (const offset of offsets) out += `${String(offset).padStart(10, '0')} 00000 n \n`;
|
|
257
|
+
out += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R /Info ${objects.length} 0 R >>\n`
|
|
258
|
+
+ `startxref\n${xrefStart}\n%%EOF`;
|
|
259
|
+
return Buffer.from(out, 'utf8');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Deliberately broken bytes for the corrupt-input contract tests (not a real
|
|
263
|
+
// xlsx / pdf).
|
|
264
|
+
const CORRUPT_XLSX_BYTES = Buffer.from('not-a-real-xlsx');
|
|
265
|
+
const CORRUPT_PDF_BYTES = Buffer.from('%PDF-1.4 placeholder\n');
|
|
266
|
+
|
|
267
|
+
const XLSX_FIXTURE = buildMinimalXlsx();
|
|
268
|
+
const PDF_FIXTURE = buildMinimalPdf();
|
|
269
|
+
|
|
270
|
+
async function loadFileNode() {
|
|
271
|
+
return import('../workflow-node-types/file-node.mjs');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function loadComponentStore() {
|
|
275
|
+
return import('../component-node-store.mjs');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function makeProject(prefix = 'wf-file-formats-') {
|
|
279
|
+
const projectRoot = makeHarnessTempRoot(prefix);
|
|
280
|
+
fs.mkdirSync(path.join(projectRoot, 'Harness', 'tasks'), { recursive: true });
|
|
281
|
+
fs.mkdirSync(path.join(projectRoot, 'src'), { recursive: true });
|
|
282
|
+
return projectRoot;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function createFileNode(projectRoot, store, filePayload) {
|
|
286
|
+
return store.createComponentNode(projectRoot, {
|
|
287
|
+
type: 'file',
|
|
288
|
+
title: filePayload.title || 'File Node',
|
|
289
|
+
file: filePayload.file,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function bindNode(projectRoot, store, relPath, mime) {
|
|
294
|
+
return createFileNode(projectRoot, store, {
|
|
295
|
+
file: { path: relPath, mime, name: path.basename(relPath) },
|
|
296
|
+
}).node.nodeId;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function is4xxError(err) {
|
|
300
|
+
return Boolean(err) && typeof err.statusCode === 'number'
|
|
301
|
+
&& err.statusCode >= 400 && err.statusCode < 500;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ---- AC-5 Excel -----------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
test('AC-5 readXlsx: a corrupt xlsx file throws a clear machine-readable error', async () => {
|
|
307
|
+
const projectRoot = makeProject();
|
|
308
|
+
try {
|
|
309
|
+
const store = await loadComponentStore();
|
|
310
|
+
const fileNode = await loadFileNode();
|
|
311
|
+
fs.writeFileSync(path.join(projectRoot, 'src', 'data.xlsx'), CORRUPT_XLSX_BYTES);
|
|
312
|
+
const nodeId = bindNode(projectRoot, store, 'src/data.xlsx',
|
|
313
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
314
|
+
|
|
315
|
+
await assert.rejects(
|
|
316
|
+
fileNode.readXlsx(nodeId, projectRoot),
|
|
317
|
+
(err) => {
|
|
318
|
+
if (!err || typeof err.code !== 'string') return false;
|
|
319
|
+
assert.ok(is4xxError(err), 'corrupt xlsx must surface a 4xx status');
|
|
320
|
+
assert.match(err.code, /XLSX|PARSE|INVALID|CORRUPT|UNSUPPORTED/i,
|
|
321
|
+
'corrupt xlsx must carry a clear machine-readable code');
|
|
322
|
+
return true;
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
} finally {
|
|
326
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('AC-5 readXlsx/readXlsxSheet: sheet-name list + headers + paged rows (real minimal xlsx fixture)', async () => {
|
|
331
|
+
const projectRoot = makeProject();
|
|
332
|
+
try {
|
|
333
|
+
const store = await loadComponentStore();
|
|
334
|
+
const fileNode = await loadFileNode();
|
|
335
|
+
fs.writeFileSync(path.join(projectRoot, 'src', 'data.xlsx'), XLSX_FIXTURE);
|
|
336
|
+
const nodeId = bindNode(projectRoot, store, 'src/data.xlsx',
|
|
337
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
338
|
+
|
|
339
|
+
const info = await fileNode.readXlsx(nodeId, projectRoot);
|
|
340
|
+
assert.ok(Array.isArray(info.sheets), 'readXlsx returns the sheet-name list');
|
|
341
|
+
assert.ok(info.sheets.length >= 1, 'at least one sheet');
|
|
342
|
+
assert.ok(info.sheets.every((name) => typeof name === 'string'), 'sheet names are strings');
|
|
343
|
+
assert.deepEqual(info.sheets, ['Data'], 'fixture sheet is parsed with its real name');
|
|
344
|
+
|
|
345
|
+
const page1 = await fileNode.readXlsxSheet(nodeId, projectRoot, { sheet: info.sheets[0], page: 1, pageSize: 2 });
|
|
346
|
+
assert.ok(Array.isArray(page1.headers) && page1.headers.length >= 1,
|
|
347
|
+
'readXlsxSheet returns the column headers');
|
|
348
|
+
assert.deepEqual(page1.headers, ['Name', 'Score'], 'header row matches the fixture');
|
|
349
|
+
assert.ok(Array.isArray(page1.rows), 'readXlsxSheet returns the page rows');
|
|
350
|
+
assert.ok(page1.rows.length <= 2, 'rows are bounded by pageSize (paged, not whole-sheet)');
|
|
351
|
+
assert.deepEqual(page1.rows, [['Alice', 42], ['Bob', 17]], 'page 1 carries the first data rows');
|
|
352
|
+
|
|
353
|
+
const page2 = await fileNode.readXlsxSheet(nodeId, projectRoot, { sheet: info.sheets[0], page: 2, pageSize: 2 });
|
|
354
|
+
assert.notDeepEqual(page2.rows, page1.rows, 'page 2 yields different rows than page 1');
|
|
355
|
+
assert.deepEqual(page2.rows, [['Carol', 88], ['Dave', 5]], 'page 2 carries the remaining rows');
|
|
356
|
+
} finally {
|
|
357
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// ---- AC-6 PDF -------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
test('AC-6 readPdf: a corrupt pdf throws a clear machine-readable error', async () => {
|
|
364
|
+
const projectRoot = makeProject();
|
|
365
|
+
try {
|
|
366
|
+
const store = await loadComponentStore();
|
|
367
|
+
const fileNode = await loadFileNode();
|
|
368
|
+
fs.writeFileSync(path.join(projectRoot, 'src', 'report.pdf'), CORRUPT_PDF_BYTES);
|
|
369
|
+
const nodeId = bindNode(projectRoot, store, 'src/report.pdf', 'application/pdf');
|
|
370
|
+
|
|
371
|
+
await assert.rejects(
|
|
372
|
+
fileNode.readPdf(nodeId, projectRoot),
|
|
373
|
+
(err) => {
|
|
374
|
+
if (!err || typeof err.code !== 'string') return false;
|
|
375
|
+
assert.ok(is4xxError(err), 'corrupt pdf must surface a 4xx status');
|
|
376
|
+
assert.match(err.code, /PDF|PARSE|INVALID|CORRUPT|UNSUPPORTED/i,
|
|
377
|
+
'corrupt pdf must carry a clear machine-readable code');
|
|
378
|
+
return true;
|
|
379
|
+
},
|
|
380
|
+
);
|
|
381
|
+
} finally {
|
|
382
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('AC-6 readPdf/readPdfPage: total pages + metadata + page-range text with isEvalSupported:false baseline (real minimal pdf)', async () => {
|
|
387
|
+
const projectRoot = makeProject();
|
|
388
|
+
try {
|
|
389
|
+
const store = await loadComponentStore();
|
|
390
|
+
const fileNode = await loadFileNode();
|
|
391
|
+
fs.writeFileSync(path.join(projectRoot, 'src', 'report.pdf'), PDF_FIXTURE);
|
|
392
|
+
const nodeId = bindNode(projectRoot, store, 'src/report.pdf', 'application/pdf');
|
|
393
|
+
|
|
394
|
+
const info = await fileNode.readPdf(nodeId, projectRoot);
|
|
395
|
+
assert.ok(Number.isInteger(info.totalPages) && info.totalPages >= 1,
|
|
396
|
+
'readPdf returns the total page count');
|
|
397
|
+
assert.equal(info.totalPages, 2, 'fixture pdf has its real page count');
|
|
398
|
+
assert.ok(info.metadata && typeof info.metadata === 'object', 'readPdf returns metadata');
|
|
399
|
+
assert.equal(info.metadata.title, 'Fixture Report', 'metadata carries the real title');
|
|
400
|
+
|
|
401
|
+
const page = await fileNode.readPdfPage(nodeId, projectRoot, { page: 1, pageCount: 2 });
|
|
402
|
+
assert.equal(typeof page.text, 'string', 'readPdfPage returns page-range text');
|
|
403
|
+
assert.ok(page.text.includes('Hello PDF'), 'page-range text contains the first page text');
|
|
404
|
+
assert.ok(page.text.includes('Second Page'), 'page-range text contains the second page text');
|
|
405
|
+
assert.equal(page.isEvalSupported, false,
|
|
406
|
+
'AC-6 security baseline: PDF rendering must run with isEvalSupported:false');
|
|
407
|
+
} finally {
|
|
408
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// ---- AC-7 ZIP -------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
function writeZipFixture(projectRoot, relPath, entries) {
|
|
415
|
+
fs.writeFileSync(path.join(projectRoot, relPath), buildZip(entries));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
test('AC-7 readZipEntries: entry list with size/compressedSize/directory flags; GBK names decoded; UTF-8 flag honored', async () => {
|
|
419
|
+
const projectRoot = makeProject();
|
|
420
|
+
try {
|
|
421
|
+
const store = await loadComponentStore();
|
|
422
|
+
const fileNode = await loadFileNode();
|
|
423
|
+
writeZipFixture(projectRoot, 'src/bundle.zip', [
|
|
424
|
+
{ name: 'readme.txt', content: 'hello zip\n' },
|
|
425
|
+
{ name: 'docs/', directory: true },
|
|
426
|
+
{ name: 'big.txt', content: 'Z'.repeat(20000) },
|
|
427
|
+
{ nameBytes: GBK_NAME_BYTES, content: 'gbk content', utf8: false },
|
|
428
|
+
{ name: '中文utf8.txt', content: 'utf8 name', utf8: true },
|
|
429
|
+
]);
|
|
430
|
+
const nodeId = bindNode(projectRoot, store, 'src/bundle.zip', 'application/zip');
|
|
431
|
+
|
|
432
|
+
const entries = await fileNode.readZipEntries(nodeId, projectRoot);
|
|
433
|
+
assert.ok(Array.isArray(entries), 'readZipEntries returns the entry list');
|
|
434
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
435
|
+
|
|
436
|
+
const readme = byName.get('readme.txt');
|
|
437
|
+
assert.ok(readme, 'plain text entry is listed');
|
|
438
|
+
assert.equal(readme.size, Buffer.byteLength('hello zip\n'));
|
|
439
|
+
assert.equal(readme.directory, false);
|
|
440
|
+
|
|
441
|
+
const docs = entries.find((entry) => entry.directory === true);
|
|
442
|
+
assert.ok(docs, 'directory entry carries the directory flag');
|
|
443
|
+
assert.equal(docs.size, 0);
|
|
444
|
+
|
|
445
|
+
const big = byName.get('big.txt');
|
|
446
|
+
assert.equal(big.size, 20000);
|
|
447
|
+
assert.ok(big.compressedSize > 0 && big.compressedSize < 20000,
|
|
448
|
+
'compressedSize reflects deflation');
|
|
449
|
+
|
|
450
|
+
const gbk = byName.get('中文名.txt');
|
|
451
|
+
assert.ok(gbk, 'GBK-named entry must be decoded to 中文名.txt (iconv-lite)');
|
|
452
|
+
assert.equal(gbk.isUtf8, false, 'GBK entry must carry isUtf8:false');
|
|
453
|
+
|
|
454
|
+
const utf8 = byName.get('中文utf8.txt');
|
|
455
|
+
assert.ok(utf8, 'UTF-8-named entry is listed under its decoded name');
|
|
456
|
+
assert.equal(utf8.isUtf8, true, 'UTF-8 entry must carry isUtf8:true');
|
|
457
|
+
} finally {
|
|
458
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('AC-7 readZipEntry: single text entry preview with maxBytes truncation', async () => {
|
|
463
|
+
const projectRoot = makeProject();
|
|
464
|
+
try {
|
|
465
|
+
const store = await loadComponentStore();
|
|
466
|
+
const fileNode = await loadFileNode();
|
|
467
|
+
writeZipFixture(projectRoot, 'src/bundle.zip', [
|
|
468
|
+
{ name: 'readme.txt', content: 'hello zip\n' },
|
|
469
|
+
{ name: 'big.txt', content: 'Z'.repeat(20000) },
|
|
470
|
+
]);
|
|
471
|
+
const nodeId = bindNode(projectRoot, store, 'src/bundle.zip', 'application/zip');
|
|
472
|
+
|
|
473
|
+
const small = await fileNode.readZipEntry(nodeId, projectRoot, { entryName: 'readme.txt' });
|
|
474
|
+
assert.equal(small.text, 'hello zip\n', 'single text entry is previewed');
|
|
475
|
+
assert.equal(small.truncated, false, 'small entry is not truncated');
|
|
476
|
+
|
|
477
|
+
const capped = await fileNode.readZipEntry(nodeId, projectRoot, { entryName: 'big.txt', maxBytes: 1024 });
|
|
478
|
+
assert.equal(capped.truncated, true, 'entry larger than maxBytes is flagged truncated');
|
|
479
|
+
assert.ok(capped.text.length <= 1024, 'preview text respects the maxBytes bound');
|
|
480
|
+
assert.ok(capped.text.startsWith('Z'), 'preview starts at the entry beginning');
|
|
481
|
+
} finally {
|
|
482
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test('AC-7 extractZipEntry: single entry extracts into a workspace-controlled destDir', async () => {
|
|
487
|
+
const projectRoot = makeProject();
|
|
488
|
+
try {
|
|
489
|
+
const store = await loadComponentStore();
|
|
490
|
+
const fileNode = await loadFileNode();
|
|
491
|
+
writeZipFixture(projectRoot, 'src/bundle.zip', [
|
|
492
|
+
{ name: 'readme.txt', content: 'hello zip\n' },
|
|
493
|
+
{ name: 'skip.txt', content: 'not extracted\n' },
|
|
494
|
+
]);
|
|
495
|
+
const nodeId = bindNode(projectRoot, store, 'src/bundle.zip', 'application/zip');
|
|
496
|
+
|
|
497
|
+
const destDir = 'Harness/.extracted/zip-test';
|
|
498
|
+
const result = await fileNode.extractZipEntry(nodeId, projectRoot, { entryName: 'readme.txt', destDir });
|
|
499
|
+
assert.equal(result.ok, true, 'extraction reports success');
|
|
500
|
+
|
|
501
|
+
const outPath = path.join(projectRoot, destDir, 'readme.txt');
|
|
502
|
+
assert.equal(fs.readFileSync(outPath, 'utf8'), 'hello zip\n', 'entry content lands in destDir');
|
|
503
|
+
assert.equal(fs.existsSync(path.join(projectRoot, destDir, 'skip.txt')), false,
|
|
504
|
+
'only the requested entry is extracted');
|
|
505
|
+
} finally {
|
|
506
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test('AC-7 extractZipEntry: rejects Zip Slip entries (../ and ..\\) — nothing escapes the workspace', async () => {
|
|
511
|
+
const projectRoot = makeProject();
|
|
512
|
+
try {
|
|
513
|
+
const store = await loadComponentStore();
|
|
514
|
+
const fileNode = await loadFileNode();
|
|
515
|
+
writeZipFixture(projectRoot, 'src/evil.zip', [
|
|
516
|
+
{ name: '../evil.txt', content: 'escape me' },
|
|
517
|
+
{ name: '..\\evil2.txt', content: 'escape me too' },
|
|
518
|
+
]);
|
|
519
|
+
const nodeId = bindNode(projectRoot, store, 'src/evil.zip', 'application/zip');
|
|
520
|
+
|
|
521
|
+
const destDir = 'Harness/.extracted/zip-test';
|
|
522
|
+
await assert.rejects(
|
|
523
|
+
fileNode.extractZipEntry(nodeId, projectRoot, { entryName: '../evil.txt', destDir }),
|
|
524
|
+
/traversal|escape|invalid|path/i,
|
|
525
|
+
'forward-slash traversal entry must be rejected',
|
|
526
|
+
);
|
|
527
|
+
await assert.rejects(
|
|
528
|
+
fileNode.extractZipEntry(nodeId, projectRoot, { entryName: '..\\evil2.txt', destDir }),
|
|
529
|
+
/traversal|escape|invalid|path/i,
|
|
530
|
+
'backslash traversal entry must be rejected on Windows too',
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
assert.equal(fs.existsSync(path.join(projectRoot, destDir, 'evil.txt')), false,
|
|
534
|
+
'no traversal entry may land in destDir');
|
|
535
|
+
assert.equal(fs.existsSync(path.join(projectRoot, '..', 'evil.txt')), false,
|
|
536
|
+
'no traversal entry may land outside the workspace');
|
|
537
|
+
assert.equal(fs.existsSync(path.join(projectRoot, '..', 'evil2.txt')), false,
|
|
538
|
+
'no backslash traversal entry may land outside the workspace');
|
|
539
|
+
} finally {
|
|
540
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test('AC-7 extractZipEntry: destDir escaping the workspace is rejected', async () => {
|
|
545
|
+
const projectRoot = makeProject();
|
|
546
|
+
try {
|
|
547
|
+
const store = await loadComponentStore();
|
|
548
|
+
const fileNode = await loadFileNode();
|
|
549
|
+
writeZipFixture(projectRoot, 'src/bundle.zip', [{ name: 'readme.txt', content: 'hello zip\n' }]);
|
|
550
|
+
const nodeId = bindNode(projectRoot, store, 'src/bundle.zip', 'application/zip');
|
|
551
|
+
|
|
552
|
+
await assert.rejects(
|
|
553
|
+
fileNode.extractZipEntry(nodeId, projectRoot, { entryName: 'readme.txt', destDir: '../escape' }),
|
|
554
|
+
/workspace|traversal|escape/i,
|
|
555
|
+
'destDir outside the workspace must be rejected',
|
|
556
|
+
);
|
|
557
|
+
assert.equal(fs.existsSync(path.join(projectRoot, '..', 'escape')), false,
|
|
558
|
+
'nothing may be written outside the workspace');
|
|
559
|
+
} finally {
|
|
560
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
// W4F security-review regression tests (task-upgrade-file-node W4F wave):
|
|
566
|
+
// H1 maxBytes hard clamp (Infinity/1e999/1e15 can never raise the preview
|
|
567
|
+
// budget above 1MB; entries over the hard cap are never streamed).
|
|
568
|
+
// M1 extractZipEntry hardening: lock gate, no-overwrite, 64MB entry cap,
|
|
569
|
+
// NTFS ADS ':' rejection, Windows segment validation reuse.
|
|
570
|
+
// M2 readZipEntries entry-count cap with truncated:true.
|
|
571
|
+
// M3 xlsx/pdf FILE_TOO_LARGE gate + per-page PDF text extraction.
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
|
|
574
|
+
test('W4F-H1 maxBytes clamp: 1e999/Infinity fall back to 64KB, 1e15 caps at 1MB, >1MB entries never stream', async () => {
|
|
575
|
+
const projectRoot = makeProject();
|
|
576
|
+
try {
|
|
577
|
+
const store = await loadComponentStore();
|
|
578
|
+
const fileNode = await loadFileNode();
|
|
579
|
+
writeZipFixture(projectRoot, 'src/bundle.zip', [
|
|
580
|
+
{ name: 'readme.txt', content: 'hello zip\n' },
|
|
581
|
+
{ name: 'mid.txt', content: 'M'.repeat(100 * 1024) }, // 100KB, under the 1MB cap
|
|
582
|
+
{ name: 'huge.txt', content: 'H'.repeat(2 * 1024 * 1024) }, // 2MB, over the 1MB cap
|
|
583
|
+
]);
|
|
584
|
+
const nodeId = bindNode(projectRoot, store, 'src/bundle.zip', 'application/zip');
|
|
585
|
+
|
|
586
|
+
// Non-finite budget (1e999): clamp to the 64KB default; a small file
|
|
587
|
+
// still reads in full and the echoed maxBytes field proves the clamp.
|
|
588
|
+
const smallInfinity = await fileNode.readZipEntry(nodeId, projectRoot, { entryName: 'readme.txt', maxBytes: 1e999 });
|
|
589
|
+
assert.equal(smallInfinity.text, 'hello zip\n', '1e999 still returns the full small file');
|
|
590
|
+
assert.equal(smallInfinity.truncated, false, '1e999 does not truncate a small file');
|
|
591
|
+
assert.equal(smallInfinity.maxBytes, 65536, '1e999 clamps to the 64KB default');
|
|
592
|
+
|
|
593
|
+
const smallInfinityLiteral = await fileNode.readZipEntry(nodeId, projectRoot, { entryName: 'readme.txt', maxBytes: Infinity });
|
|
594
|
+
assert.equal(smallInfinityLiteral.maxBytes, 65536, 'literal Infinity clamps to the 64KB default');
|
|
595
|
+
|
|
596
|
+
// Finite-but-huge budget (1e15): clamp to 1MB; a sub-1MB file reads fully.
|
|
597
|
+
const mid = await fileNode.readZipEntry(nodeId, projectRoot, { entryName: 'mid.txt', maxBytes: 1e15 });
|
|
598
|
+
assert.equal(mid.maxBytes, 1048576, '1e15 clamps to the 1MB hard cap');
|
|
599
|
+
assert.equal(mid.truncated, false, 'a 100KB file is fully read under the 1MB clamp');
|
|
600
|
+
assert.equal(mid.text.length, 100 * 1024, 'the full 100KB is returned');
|
|
601
|
+
|
|
602
|
+
// A >1MB entry with an absurd budget proves the effective cap is <=1MB
|
|
603
|
+
// (an unclamped budget would have read all 2MB with truncated:false).
|
|
604
|
+
const hugeFinite = await fileNode.readZipEntry(nodeId, projectRoot, { entryName: 'huge.txt', maxBytes: 1e15 });
|
|
605
|
+
assert.equal(hugeFinite.truncated, true, 'a 2MB entry cannot fit the 1MB clamp');
|
|
606
|
+
assert.ok(hugeFinite.text.length <= 1048576, 'preview text respects the 1MB clamp');
|
|
607
|
+
assert.equal(hugeFinite.maxBytes, 1048576);
|
|
608
|
+
|
|
609
|
+
// Over-cap entries are refused WITHOUT opening a read stream: the
|
|
610
|
+
// truncation marker carries no decompressed bytes at all.
|
|
611
|
+
const hugeInfinity = await fileNode.readZipEntry(nodeId, projectRoot, { entryName: 'huge.txt', maxBytes: 1e999 });
|
|
612
|
+
assert.equal(hugeInfinity.text, '', 'a >1MB entry returns the empty truncation marker (no stream)');
|
|
613
|
+
assert.equal(hugeInfinity.truncated, true);
|
|
614
|
+
assert.equal(hugeInfinity.maxBytes, 65536);
|
|
615
|
+
|
|
616
|
+
// Entries UP TO the hard cap keep the streamed-prefix behavior (existing
|
|
617
|
+
// AC-7 contract: preview starts at the entry beginning).
|
|
618
|
+
const midSmallBudget = await fileNode.readZipEntry(nodeId, projectRoot, { entryName: 'mid.txt', maxBytes: 1024 });
|
|
619
|
+
assert.equal(midSmallBudget.truncated, true, 'mid-size entry at a small budget is flagged truncated');
|
|
620
|
+
assert.equal(midSmallBudget.text.length, 1024, 'mid-size entry streams exactly the budgeted prefix');
|
|
621
|
+
assert.ok(midSmallBudget.text.startsWith('M'), 'mid-size entry preview starts at the entry beginning');
|
|
622
|
+
} finally {
|
|
623
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test('W4F-M1 extractZipEntry: refuses overwrite of an existing target with 409 FILE_EXISTS', async () => {
|
|
628
|
+
const projectRoot = makeProject();
|
|
629
|
+
try {
|
|
630
|
+
const store = await loadComponentStore();
|
|
631
|
+
const fileNode = await loadFileNode();
|
|
632
|
+
writeZipFixture(projectRoot, 'src/bundle.zip', [
|
|
633
|
+
{ name: 'readme.txt', content: 'hello zip\n' },
|
|
634
|
+
{ name: 'skip.txt', content: 'not extracted\n' },
|
|
635
|
+
]);
|
|
636
|
+
const nodeId = bindNode(projectRoot, store, 'src/bundle.zip', 'application/zip');
|
|
637
|
+
const destDir = 'Harness/.extracted/zip-test';
|
|
638
|
+
|
|
639
|
+
const first = await fileNode.extractZipEntry(nodeId, projectRoot, { entryName: 'readme.txt', destDir });
|
|
640
|
+
assert.equal(first.ok, true, 'first extraction succeeds');
|
|
641
|
+
|
|
642
|
+
await assert.rejects(
|
|
643
|
+
fileNode.extractZipEntry(nodeId, projectRoot, { entryName: 'readme.txt', destDir }),
|
|
644
|
+
(err) => {
|
|
645
|
+
if (!err || typeof err.code !== 'string') return false;
|
|
646
|
+
assert.equal(err.code, 'FILE_EXISTS', 'overwrite attempt must carry code FILE_EXISTS');
|
|
647
|
+
assert.equal(err.statusCode, 409, 'overwrite attempt must be HTTP 409');
|
|
648
|
+
return true;
|
|
649
|
+
},
|
|
650
|
+
'extracting onto an existing file must be refused (requireMissing semantics)',
|
|
651
|
+
);
|
|
652
|
+
assert.equal(fs.readFileSync(path.join(projectRoot, destDir, 'readme.txt'), 'utf8'), 'hello zip\n',
|
|
653
|
+
'the rejected overwrite must leave the existing file untouched');
|
|
654
|
+
} finally {
|
|
655
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('W4F-M1 extractZipEntry: foreign lock refuses with 409 file_locked before any write', async () => {
|
|
660
|
+
const projectRoot = makeProject();
|
|
661
|
+
try {
|
|
662
|
+
const store = await loadComponentStore();
|
|
663
|
+
const fileNode = await loadFileNode();
|
|
664
|
+
writeZipFixture(projectRoot, 'src/bundle.zip', [{ name: 'readme.txt', content: 'hello zip\n' }]);
|
|
665
|
+
const nodeId = bindNode(projectRoot, store, 'src/bundle.zip', 'application/zip');
|
|
666
|
+
|
|
667
|
+
const lock = store.acquireLock(nodeId, 'agent-a', 60000, { projectRoot });
|
|
668
|
+
assert.ok(lock.lockId, 'lock acquired for the extract gate test');
|
|
669
|
+
|
|
670
|
+
await assert.rejects(
|
|
671
|
+
fileNode.extractZipEntry(nodeId, projectRoot, { entryName: 'readme.txt', destDir: 'Harness/.extracted/locked' }),
|
|
672
|
+
(err) => {
|
|
673
|
+
if (!err || typeof err.code !== 'string') return false;
|
|
674
|
+
assert.equal(err.code, 'file_locked', 'extract under a foreign lease must carry code file_locked');
|
|
675
|
+
assert.equal(err.statusCode, 409, 'extract lock conflict must be HTTP 409');
|
|
676
|
+
return true;
|
|
677
|
+
},
|
|
678
|
+
'extract must honor the same lock gate as file.writeText',
|
|
679
|
+
);
|
|
680
|
+
assert.equal(fs.existsSync(path.join(projectRoot, 'Harness', '.extracted', 'locked', 'readme.txt')), false,
|
|
681
|
+
'nothing may be written under a foreign lease');
|
|
682
|
+
} finally {
|
|
683
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test('W4F-M1 extractZipEntry: central-directory uncompressed size over 64MB is refused before writing', async () => {
|
|
688
|
+
const projectRoot = makeProject();
|
|
689
|
+
try {
|
|
690
|
+
const store = await loadComponentStore();
|
|
691
|
+
const fileNode = await loadFileNode();
|
|
692
|
+
// The archive payload is tiny; the central-directory usize is the test
|
|
693
|
+
// seam that simulates a lying 64MB+1 uncompressed size (64MB cap + 1).
|
|
694
|
+
writeZipFixture(projectRoot, 'src/huge.zip', [
|
|
695
|
+
{ name: 'big.bin', content: 'tiny', usize: 64 * 1024 * 1024 + 1 },
|
|
696
|
+
]);
|
|
697
|
+
const nodeId = bindNode(projectRoot, store, 'src/huge.zip', 'application/zip');
|
|
698
|
+
|
|
699
|
+
await assert.rejects(
|
|
700
|
+
fileNode.extractZipEntry(nodeId, projectRoot, { entryName: 'big.bin', destDir: 'Harness/.extracted/big' }),
|
|
701
|
+
(err) => {
|
|
702
|
+
if (!err || typeof err.code !== 'string') return false;
|
|
703
|
+
assert.equal(err.code, 'PAYLOAD_TOO_LARGE', 'oversized entry must carry code PAYLOAD_TOO_LARGE');
|
|
704
|
+
assert.equal(err.statusCode, 413, 'oversized entry must be HTTP 413');
|
|
705
|
+
return true;
|
|
706
|
+
},
|
|
707
|
+
'an entry over the 64MB extract cap must be refused before any byte is written',
|
|
708
|
+
);
|
|
709
|
+
assert.equal(fs.existsSync(path.join(projectRoot, 'Harness', '.extracted', 'big', 'big.bin')), false,
|
|
710
|
+
'nothing may be written for an over-cap entry');
|
|
711
|
+
} finally {
|
|
712
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test('W4F-M1 extractZipEntry: NTFS ADS (colon) and Windows-reserved entry names are rejected', async () => {
|
|
717
|
+
const projectRoot = makeProject();
|
|
718
|
+
try {
|
|
719
|
+
const store = await loadComponentStore();
|
|
720
|
+
const fileNode = await loadFileNode();
|
|
721
|
+
writeZipFixture(projectRoot, 'src/evil.zip', [
|
|
722
|
+
{ name: 'notes.txt:evil', content: 'ads payload' },
|
|
723
|
+
{ name: 'CON.txt', content: 'reserved name' },
|
|
724
|
+
{ name: 'trail.txt.', content: 'trailing dot' },
|
|
725
|
+
]);
|
|
726
|
+
const nodeId = bindNode(projectRoot, store, 'src/evil.zip', 'application/zip');
|
|
727
|
+
|
|
728
|
+
await assert.rejects(
|
|
729
|
+
fileNode.extractZipEntry(nodeId, projectRoot, { entryName: 'notes.txt:evil', destDir: 'Harness/.extracted/ads' }),
|
|
730
|
+
(err) => {
|
|
731
|
+
if (!err || typeof err.code !== 'string') return false;
|
|
732
|
+
assert.equal(err.code, 'ZIP_INVALID_NAME', 'colon entry must carry code ZIP_INVALID_NAME');
|
|
733
|
+
assert.equal(err.statusCode, 400, 'colon entry must be HTTP 400');
|
|
734
|
+
return true;
|
|
735
|
+
},
|
|
736
|
+
'NTFS ADS colon entry names must be rejected regardless of position',
|
|
737
|
+
);
|
|
738
|
+
assert.equal(fs.existsSync(path.join(projectRoot, 'Harness', '.extracted', 'ads', 'evil')), false,
|
|
739
|
+
'no ADS payload may land in destDir');
|
|
740
|
+
|
|
741
|
+
await assert.rejects(
|
|
742
|
+
fileNode.extractZipEntry(nodeId, projectRoot, { entryName: 'CON.txt', destDir: 'Harness/.extracted/reserved' }),
|
|
743
|
+
(err) => {
|
|
744
|
+
if (!err || typeof err.code !== 'string') return false;
|
|
745
|
+
assert.ok(is4xxError(err), 'reserved-name entry must surface a 4xx status');
|
|
746
|
+
return true;
|
|
747
|
+
},
|
|
748
|
+
'Windows reserved names must be rejected via the workspace segment validation',
|
|
749
|
+
);
|
|
750
|
+
assert.equal(fs.existsSync(path.join(projectRoot, 'Harness', '.extracted', 'reserved', 'CON.txt')), false,
|
|
751
|
+
'no reserved-name payload may land in destDir');
|
|
752
|
+
|
|
753
|
+
await assert.rejects(
|
|
754
|
+
fileNode.extractZipEntry(nodeId, projectRoot, { entryName: 'trail.txt.', destDir: 'Harness/.extracted/trail' }),
|
|
755
|
+
(err) => {
|
|
756
|
+
if (!err || typeof err.code !== 'string') return false;
|
|
757
|
+
assert.ok(is4xxError(err), 'trailing-dot entry must surface a 4xx status');
|
|
758
|
+
return true;
|
|
759
|
+
},
|
|
760
|
+
'trailing-dot entry names must be rejected via the workspace segment validation',
|
|
761
|
+
);
|
|
762
|
+
} finally {
|
|
763
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test('W4F-M2 readZipEntries: entry scan is capped with truncated:true (injectable limit)', async () => {
|
|
768
|
+
const projectRoot = makeProject();
|
|
769
|
+
try {
|
|
770
|
+
const adapters = await import('../file-format-adapters.mjs');
|
|
771
|
+
const zipAbs = path.join(projectRoot, 'src', 'many.zip');
|
|
772
|
+
writeZipFixture(projectRoot, 'src/many.zip',
|
|
773
|
+
Array.from({ length: 8 }, (unused, i) => ({ name: `f${i}.txt`, content: `c${i}` })));
|
|
774
|
+
|
|
775
|
+
const capped = await adapters.readZipEntries(zipAbs, { maxEntries: 5 });
|
|
776
|
+
assert.equal(capped.truncated, true, 'over-cap scan must report truncated:true');
|
|
777
|
+
assert.ok(Array.isArray(capped.entries) && capped.entries.length <= 5,
|
|
778
|
+
'truncated scan returns at most maxEntries entries');
|
|
779
|
+
assert.equal(capped.total, null, 'truncated scan reports an unknown total');
|
|
780
|
+
|
|
781
|
+
const full = await adapters.readZipEntries(zipAbs);
|
|
782
|
+
assert.equal(full.truncated, false, 'an under-cap scan is not truncated');
|
|
783
|
+
assert.equal(full.entries.length, 8, 'the default scan returns every entry');
|
|
784
|
+
assert.equal(full.total, 8, 'a full scan reports the actual entry count');
|
|
785
|
+
|
|
786
|
+
// The injectable limit can never RAISE the scan budget above the
|
|
787
|
+
// MAX_ZIP_ENTRIES constant (cap-the-cap).
|
|
788
|
+
const overRidden = await adapters.readZipEntries(zipAbs, { maxEntries: 1e12 });
|
|
789
|
+
assert.equal(overRidden.truncated, false, 'an absurd maxEntries is clamped to the hard cap');
|
|
790
|
+
assert.equal(overRidden.entries.length, 8);
|
|
791
|
+
} finally {
|
|
792
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test('W4F-M3 xlsx/pdf size gate: oversized files are refused with 400 FILE_TOO_LARGE before parsing', async () => {
|
|
797
|
+
const projectRoot = makeProject();
|
|
798
|
+
try {
|
|
799
|
+
const store = await loadComponentStore();
|
|
800
|
+
const fileNode = await loadFileNode();
|
|
801
|
+
fs.writeFileSync(path.join(projectRoot, 'src', 'data.xlsx'), XLSX_FIXTURE);
|
|
802
|
+
fs.writeFileSync(path.join(projectRoot, 'src', 'report.pdf'), PDF_FIXTURE);
|
|
803
|
+
const xlsxNodeId = bindNode(projectRoot, store, 'src/data.xlsx',
|
|
804
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
805
|
+
const pdfNodeId = bindNode(projectRoot, store, 'src/report.pdf', 'application/pdf');
|
|
806
|
+
|
|
807
|
+
const assertFileTooLarge = (err) => {
|
|
808
|
+
if (!err || typeof err.code !== 'string') return false;
|
|
809
|
+
assert.equal(err.code, 'FILE_TOO_LARGE', 'oversized format file must carry code FILE_TOO_LARGE');
|
|
810
|
+
assert.equal(err.statusCode, 400, 'oversized format file must be HTTP 400');
|
|
811
|
+
return true;
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
await assert.rejects(fileNode.readXlsxSheet(xlsxNodeId, projectRoot, { maxBytes: 10 }), assertFileTooLarge,
|
|
815
|
+
'xlsx sheet read must be gated by the injectable size limit');
|
|
816
|
+
await assert.rejects(fileNode.readXlsx(xlsxNodeId, projectRoot, { maxBytes: 10 }), assertFileTooLarge,
|
|
817
|
+
'xlsx sheet-list read must be gated by the injectable size limit');
|
|
818
|
+
await assert.rejects(fileNode.readPdfPage(pdfNodeId, projectRoot, { maxBytes: 10 }), assertFileTooLarge,
|
|
819
|
+
'pdf page read must be gated by the injectable size limit');
|
|
820
|
+
await assert.rejects(fileNode.readPdf(pdfNodeId, projectRoot, { maxBytes: 10 }), assertFileTooLarge,
|
|
821
|
+
'pdf info read must be gated by the injectable size limit');
|
|
822
|
+
|
|
823
|
+
// The production default (100MB) still parses the fixtures.
|
|
824
|
+
const page = await fileNode.readPdfPage(pdfNodeId, projectRoot, { page: 1, pageCount: 1 });
|
|
825
|
+
assert.ok(page.text.includes('Hello PDF'), 'default limit still parses the fixture');
|
|
826
|
+
const sheets = await fileNode.readXlsx(xlsxNodeId, projectRoot);
|
|
827
|
+
assert.deepEqual(sheets.sheets, ['Data'], 'default limit still parses the xlsx fixture');
|
|
828
|
+
} finally {
|
|
829
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
test('W4F-M3 readPdfPage: per-page extraction returns only the requested page range', async () => {
|
|
834
|
+
const projectRoot = makeProject();
|
|
835
|
+
try {
|
|
836
|
+
const store = await loadComponentStore();
|
|
837
|
+
const fileNode = await loadFileNode();
|
|
838
|
+
fs.writeFileSync(path.join(projectRoot, 'src', 'report.pdf'), PDF_FIXTURE);
|
|
839
|
+
const pdfNodeId = bindNode(projectRoot, store, 'src/report.pdf', 'application/pdf');
|
|
840
|
+
|
|
841
|
+
const page2 = await fileNode.readPdfPage(pdfNodeId, projectRoot, { page: 2, pageCount: 1 });
|
|
842
|
+
assert.equal(page2.page, 2, 'the requested page is reported');
|
|
843
|
+
assert.equal(page2.pageCount, 1, 'only the requested single page is returned');
|
|
844
|
+
assert.ok(page2.text.includes('Second Page'), 'page 2 text is present');
|
|
845
|
+
assert.ok(!page2.text.includes('Hello PDF'), 'page 1 text is NOT extracted for a page-2-only read');
|
|
846
|
+
assert.equal(page2.isEvalSupported, false, 'AC-6 security baseline is preserved per page');
|
|
847
|
+
|
|
848
|
+
const over = await fileNode.readPdfPage(pdfNodeId, projectRoot, { page: 99, pageCount: 5 });
|
|
849
|
+
assert.equal(over.page, 2, 'an out-of-range start page clamps to the last page');
|
|
850
|
+
assert.ok(over.text.includes('Second Page'), 'clamped read still returns the last page text');
|
|
851
|
+
} finally {
|
|
852
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
// ---------------------------------------------------------------------------
|
|
857
|
+
// W5F final-wave regression tests (task-upgrade-file-node W5F wave):
|
|
858
|
+
// R1 format-gate maxBytes fail-open fix: clampFormatMaxBytes hard-clamps
|
|
859
|
+
// absurd budgets (1e999 / 'abc') to MAX_FORMAT_FILE_BYTES so the xlsx /
|
|
860
|
+
// pdf size gate can never be disabled; readPdfPage pageCount is capped
|
|
861
|
+
// at PDF_PAGE_COUNT_CAP so 1e9 cannot force whole-document extraction.
|
|
862
|
+
// R2 extractZipEntry byte counting: the write stream counts bytes as they
|
|
863
|
+
// land (the central-directory declared size can lie), destroys the
|
|
864
|
+
// write over the effective cap with 413 PAYLOAD_TOO_LARGE, and cleans
|
|
865
|
+
// up the half-written target on any write-path failure.
|
|
866
|
+
// ---------------------------------------------------------------------------
|
|
867
|
+
|
|
868
|
+
test('W5F-R1 clampFormatMaxBytes: absurd/NaN budgets fall back to the 100MB cap; finite budgets clamp but pass through', async () => {
|
|
869
|
+
const adapters = await import('../file-format-adapters.mjs');
|
|
870
|
+
assert.equal(adapters.clampFormatMaxBytes(1e999), adapters.MAX_FORMAT_FILE_BYTES,
|
|
871
|
+
'1e999 (Infinity) must fall back to the 100MB format cap');
|
|
872
|
+
assert.equal(adapters.clampFormatMaxBytes(Infinity), adapters.MAX_FORMAT_FILE_BYTES,
|
|
873
|
+
'literal Infinity must fall back to the 100MB format cap');
|
|
874
|
+
assert.equal(adapters.clampFormatMaxBytes('abc'), adapters.MAX_FORMAT_FILE_BYTES,
|
|
875
|
+
'NaN budget must fall back to the 100MB format cap');
|
|
876
|
+
assert.equal(adapters.clampFormatMaxBytes(1e15), adapters.MAX_FORMAT_FILE_BYTES,
|
|
877
|
+
'a finite budget above the cap is capped at 100MB');
|
|
878
|
+
assert.equal(adapters.clampFormatMaxBytes(adapters.MAX_FORMAT_FILE_BYTES), adapters.MAX_FORMAT_FILE_BYTES,
|
|
879
|
+
'the cap itself passes through');
|
|
880
|
+
assert.equal(adapters.clampFormatMaxBytes(50), 50,
|
|
881
|
+
'a small finite budget passes through unchanged');
|
|
882
|
+
assert.equal(adapters.clampFormatMaxBytes(0), 1, 'a zero budget is floored at 1');
|
|
883
|
+
assert.equal(adapters.clampFormatMaxBytes(-5), 1, 'a negative budget is floored at 1');
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
test('W5F-R1 readPdfPage: pageCount is capped at 100 — an absurd pageCount cannot force whole-document extraction', async () => {
|
|
887
|
+
const projectRoot = makeProject();
|
|
888
|
+
try {
|
|
889
|
+
const store = await loadComponentStore();
|
|
890
|
+
const fileNode = await loadFileNode();
|
|
891
|
+
const adapters = await import('../file-format-adapters.mjs');
|
|
892
|
+
fs.writeFileSync(path.join(projectRoot, 'src', 'report.pdf'), PDF_FIXTURE);
|
|
893
|
+
const pdfNodeId = bindNode(projectRoot, store, 'src/report.pdf', 'application/pdf');
|
|
894
|
+
|
|
895
|
+
const capped = await fileNode.readPdfPage(pdfNodeId, projectRoot, { page: 1, pageCount: 1e9 });
|
|
896
|
+
assert.equal(capped.pageCount, adapters.PDF_PAGE_COUNT_CAP,
|
|
897
|
+
'pageCount must be clamped to the 100-page cap');
|
|
898
|
+
assert.equal(capped.pageCount, 100, 'the clamped cap value is 100');
|
|
899
|
+
assert.equal(capped.page, 1, 'the start page is preserved');
|
|
900
|
+
assert.ok(capped.text.includes('Hello PDF'), 'page 1 text is present');
|
|
901
|
+
assert.ok(capped.text.includes('Second Page'), 'page 2 text is present');
|
|
902
|
+
assert.equal((capped.text.match(/Hello PDF/g) || []).length, 1,
|
|
903
|
+
'a 2-page fixture yields exactly one page-1 text fragment (no repeated extraction)');
|
|
904
|
+
assert.equal((capped.text.match(/Second Page/g) || []).length, 1,
|
|
905
|
+
'a 2-page fixture yields exactly one page-2 text fragment');
|
|
906
|
+
assert.equal(capped.isEvalSupported, false, 'AC-6 security baseline is preserved');
|
|
907
|
+
|
|
908
|
+
const nan = await fileNode.readPdfPage(pdfNodeId, projectRoot, { page: 2, pageCount: 'abc' });
|
|
909
|
+
assert.equal(nan.pageCount, 1, 'NaN pageCount falls back to a single page');
|
|
910
|
+
assert.ok(nan.text.includes('Second Page'), 'the single requested page is still returned');
|
|
911
|
+
} finally {
|
|
912
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
test('W5F-R2 extractZipEntry: streamed byte counting caps the write even when the declared size lies', async () => {
|
|
917
|
+
const projectRoot = makeProject();
|
|
918
|
+
try {
|
|
919
|
+
const store = await loadComponentStore();
|
|
920
|
+
const fileNode = await loadFileNode();
|
|
921
|
+
const adapters = await import('../file-format-adapters.mjs');
|
|
922
|
+
// The central-directory usize lies (declared 4, inside the injected cap);
|
|
923
|
+
// the real payload is 20 bytes — only streamed byte counting can stop it.
|
|
924
|
+
writeZipFixture(projectRoot, 'src/liar.zip', [
|
|
925
|
+
{ name: 'big.bin', content: 'Y'.repeat(20), usize: 4 },
|
|
926
|
+
]);
|
|
927
|
+
const zipAbs = path.join(projectRoot, 'src', 'liar.zip');
|
|
928
|
+
const destDirAbs = path.join(projectRoot, 'Harness', '.extracted', 'bytecap');
|
|
929
|
+
|
|
930
|
+
await assert.rejects(
|
|
931
|
+
adapters.extractZipEntry(zipAbs, 'big.bin', destDirAbs, { maxBytes: 4 }),
|
|
932
|
+
(err) => {
|
|
933
|
+
if (!err || typeof err.code !== 'string') return false;
|
|
934
|
+
assert.equal(err.code, 'PAYLOAD_TOO_LARGE',
|
|
935
|
+
'an over-limit streamed entry must carry code PAYLOAD_TOO_LARGE');
|
|
936
|
+
assert.equal(err.statusCode, 413, 'an over-limit streamed entry must be HTTP 413');
|
|
937
|
+
return true;
|
|
938
|
+
},
|
|
939
|
+
'a 20-byte payload must be refused at the injected 4-byte stream cap',
|
|
940
|
+
);
|
|
941
|
+
assert.equal(fs.existsSync(path.join(destDirAbs, 'big.bin')), false,
|
|
942
|
+
'the half-written target must be cleaned up on failure');
|
|
943
|
+
|
|
944
|
+
// The node action forwards payload.maxBytes to the adapter cap.
|
|
945
|
+
writeZipFixture(projectRoot, 'src/liar2.zip', [
|
|
946
|
+
{ name: 'big2.bin', content: 'Z'.repeat(20), usize: 4 },
|
|
947
|
+
]);
|
|
948
|
+
const actionNodeId = bindNode(projectRoot, store, 'src/liar2.zip', 'application/zip');
|
|
949
|
+
await assert.rejects(
|
|
950
|
+
fileNode.extractZipEntry(actionNodeId, projectRoot, {
|
|
951
|
+
entryName: 'big2.bin', destDir: 'Harness/.extracted/action-cap', maxBytes: 4,
|
|
952
|
+
}),
|
|
953
|
+
(err) => {
|
|
954
|
+
if (!err || typeof err.code !== 'string') return false;
|
|
955
|
+
assert.equal(err.code, 'PAYLOAD_TOO_LARGE', 'the node action must forward the byte cap');
|
|
956
|
+
assert.equal(err.statusCode, 413, 'the node action refusal must be HTTP 413');
|
|
957
|
+
return true;
|
|
958
|
+
},
|
|
959
|
+
'the file node action must enforce the injected stream cap too',
|
|
960
|
+
);
|
|
961
|
+
assert.equal(fs.existsSync(path.join(projectRoot, 'Harness', '.extracted', 'action-cap', 'big2.bin')), false,
|
|
962
|
+
'no half-written target may remain after the action-level refusal');
|
|
963
|
+
|
|
964
|
+
// Cap-the-cap: an absurd budget cannot raise the effective extract limit —
|
|
965
|
+
// the declared-size precheck still refuses a 64MB+1 entry.
|
|
966
|
+
writeZipFixture(projectRoot, 'src/huge2.zip', [
|
|
967
|
+
{ name: 'huge.bin', content: 'x', usize: adapters.MAX_EXTRACT_ENTRY_BYTES + 1 },
|
|
968
|
+
]);
|
|
969
|
+
const hugeAbs = path.join(projectRoot, 'src', 'huge2.zip');
|
|
970
|
+
const hugeDest = path.join(projectRoot, 'Harness', '.extracted', 'huge-cap');
|
|
971
|
+
const assert413 = (err) => {
|
|
972
|
+
if (!err || typeof err.code !== 'string') return false;
|
|
973
|
+
assert.equal(err.code, 'PAYLOAD_TOO_LARGE', 'over-cap entry must carry code PAYLOAD_TOO_LARGE');
|
|
974
|
+
assert.equal(err.statusCode, 413, 'over-cap entry must be HTTP 413');
|
|
975
|
+
return true;
|
|
976
|
+
};
|
|
977
|
+
await assert.rejects(adapters.extractZipEntry(hugeAbs, 'huge.bin', hugeDest, { maxBytes: 1e999 }), assert413,
|
|
978
|
+
'1e999 cannot disable the extract size gate');
|
|
979
|
+
await assert.rejects(adapters.extractZipEntry(hugeAbs, 'huge.bin', hugeDest, { maxBytes: 'abc' }), assert413,
|
|
980
|
+
'NaN cannot disable the extract size gate');
|
|
981
|
+
assert.equal(fs.existsSync(path.join(hugeDest, 'huge.bin')), false,
|
|
982
|
+
'nothing may be written for an over-cap entry');
|
|
983
|
+
|
|
984
|
+
// Success path with an explicit budget: entries within the cap still
|
|
985
|
+
// extract fully.
|
|
986
|
+
const okDest = path.join(projectRoot, 'Harness', '.extracted', 'ok-cap');
|
|
987
|
+
const ok = await adapters.extractZipEntry(zipAbs, 'big.bin', okDest, { maxBytes: 1024 });
|
|
988
|
+
assert.equal(ok.bytes, 20, 'an entry within the injected budget extracts fully');
|
|
989
|
+
assert.equal(fs.readFileSync(path.join(okDest, 'big.bin'), 'utf8'), 'Y'.repeat(20),
|
|
990
|
+
'extracted content is complete and intact');
|
|
991
|
+
} finally {
|
|
992
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
993
|
+
}
|
|
994
|
+
});
|