relay-ide 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +259 -0
- package/dist/bin/claude-remote-cli.js +390 -0
- package/dist/bin/relay-ide.js +390 -0
- package/dist/frontend/assets/abap-BdImnpbu.js +1 -0
- package/dist/frontend/assets/actionscript-3-CoDkCxhg.js +1 -0
- package/dist/frontend/assets/ada-bCR0ucgS.js +1 -0
- package/dist/frontend/assets/andromeeda-C4gqWexZ.js +1 -0
- package/dist/frontend/assets/angular-html-DA-rfuFy.js +1 -0
- package/dist/frontend/assets/angular-ts-BrjP3tb8.js +1 -0
- package/dist/frontend/assets/apache-Pmp26Uib.js +1 -0
- package/dist/frontend/assets/apex-D8_7TLub.js +1 -0
- package/dist/frontend/assets/apl-CORt7UWP.js +1 -0
- package/dist/frontend/assets/applescript-Co6uUVPk.js +1 -0
- package/dist/frontend/assets/ara-BRHolxvo.js +1 -0
- package/dist/frontend/assets/asciidoc-Ve4PFQV2.js +1 -0
- package/dist/frontend/assets/asm-D_Q5rh1f.js +1 -0
- package/dist/frontend/assets/astro-HNnZUWAn.js +1 -0
- package/dist/frontend/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/frontend/assets/awk-DMzUqQB5.js +1 -0
- package/dist/frontend/assets/ayu-dark-DYE7WIF3.js +1 -0
- package/dist/frontend/assets/ayu-light-BA47KaF1.js +1 -0
- package/dist/frontend/assets/ayu-mirage-32ctXXKs.js +1 -0
- package/dist/frontend/assets/ballerina-BFfxhgS-.js +1 -0
- package/dist/frontend/assets/bat-BkioyH1T.js +1 -0
- package/dist/frontend/assets/beancount-k_qm7-4y.js +1 -0
- package/dist/frontend/assets/berry-uYugtg8r.js +1 -0
- package/dist/frontend/assets/bibtex-CHM0blh-.js +1 -0
- package/dist/frontend/assets/bicep-Bmn6On1c.js +1 -0
- package/dist/frontend/assets/bird2-BIv1doCn.js +1 -0
- package/dist/frontend/assets/blade-BjGOyj-B.js +1 -0
- package/dist/frontend/assets/bsl-BO_Y6i37.js +1 -0
- package/dist/frontend/assets/c-BIGW1oBm.js +1 -0
- package/dist/frontend/assets/c3-eo99z4R2.js +1 -0
- package/dist/frontend/assets/cadence-Bv_4Rxtq.js +1 -0
- package/dist/frontend/assets/cairo-KRGpt6FW.js +1 -0
- package/dist/frontend/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- package/dist/frontend/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- package/dist/frontend/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- package/dist/frontend/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- package/dist/frontend/assets/clarity-D53aC0YG.js +1 -0
- package/dist/frontend/assets/clojure-P80f7IUj.js +1 -0
- package/dist/frontend/assets/cmake-D1j8_8rp.js +1 -0
- package/dist/frontend/assets/cobol-nBiQ_Alo.js +1 -0
- package/dist/frontend/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/frontend/assets/codeql-DsOJ9woJ.js +1 -0
- package/dist/frontend/assets/coffee-Ch7k5sss.js +1 -0
- package/dist/frontend/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/dist/frontend/assets/coq-DkFqJrB1.js +1 -0
- package/dist/frontend/assets/cpp-CofmeUqb.js +1 -0
- package/dist/frontend/assets/crystal-DNxU26gB.js +1 -0
- package/dist/frontend/assets/csharp-COcwbKMJ.js +1 -0
- package/dist/frontend/assets/css-CLj8gQPS.js +1 -0
- package/dist/frontend/assets/csv-fuZLfV_i.js +1 -0
- package/dist/frontend/assets/cue-D82EKSYY.js +1 -0
- package/dist/frontend/assets/cypher-COkxafJQ.js +1 -0
- package/dist/frontend/assets/d-85-TOEBH.js +1 -0
- package/dist/frontend/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/frontend/assets/dart-bE4Kk8sk.js +1 -0
- package/dist/frontend/assets/dax-CEL-wOlO.js +1 -0
- package/dist/frontend/assets/desktop-BmXAJ9_W.js +1 -0
- package/dist/frontend/assets/diff-D97Zzqfu.js +1 -0
- package/dist/frontend/assets/docker-BcOcwvcX.js +1 -0
- package/dist/frontend/assets/dotenv-Da5cRb03.js +1 -0
- package/dist/frontend/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/frontend/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/frontend/assets/dream-maker-BtqSS_iP.js +1 -0
- package/dist/frontend/assets/edge-FbVlp4U3.js +1 -0
- package/dist/frontend/assets/elixir-CkH2-t6x.js +1 -0
- package/dist/frontend/assets/elm-DbKCFpqz.js +1 -0
- package/dist/frontend/assets/emacs-lisp-CXvaQtF9.js +1 -0
- package/dist/frontend/assets/erb-BYCe7drp.js +1 -0
- package/dist/frontend/assets/erlang-DsQrWhSR.js +1 -0
- package/dist/frontend/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/frontend/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/frontend/assets/fennel-BYunw83y.js +1 -0
- package/dist/frontend/assets/fish-BvzEVeQv.js +1 -0
- package/dist/frontend/assets/fluent-C4IJs8-o.js +1 -0
- package/dist/frontend/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- package/dist/frontend/assets/fortran-free-form-BxgE0vQu.js +1 -0
- package/dist/frontend/assets/fsharp-CXgrBDvD.js +1 -0
- package/dist/frontend/assets/gdresource-BOOCDP_w.js +1 -0
- package/dist/frontend/assets/gdscript-C5YyOfLZ.js +1 -0
- package/dist/frontend/assets/gdshader-DkwncUOv.js +1 -0
- package/dist/frontend/assets/genie-D0YGMca9.js +1 -0
- package/dist/frontend/assets/gherkin-DyxjwDmM.js +1 -0
- package/dist/frontend/assets/git-commit-F4YmCXRG.js +1 -0
- package/dist/frontend/assets/git-rebase-r7XF79zn.js +1 -0
- package/dist/frontend/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/frontend/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/frontend/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/frontend/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/frontend/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/frontend/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/frontend/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/frontend/assets/gleam-BspZqrRM.js +1 -0
- package/dist/frontend/assets/glimmer-js-ByusRIyA.js +1 -0
- package/dist/frontend/assets/glimmer-ts-BfAWNZQY.js +1 -0
- package/dist/frontend/assets/glsl-DplSGwfg.js +1 -0
- package/dist/frontend/assets/gn-n2N0HUVH.js +1 -0
- package/dist/frontend/assets/gnuplot-DdkO51Og.js +1 -0
- package/dist/frontend/assets/go-C27-OAKa.js +1 -0
- package/dist/frontend/assets/graphql-ChdNCCLP.js +1 -0
- package/dist/frontend/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/frontend/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/dist/frontend/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/dist/frontend/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/dist/frontend/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/dist/frontend/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/dist/frontend/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/dist/frontend/assets/hack-i7_Ulhet.js +1 -0
- package/dist/frontend/assets/haml-D5jkg6IW.js +1 -0
- package/dist/frontend/assets/handlebars-BpdQsYii.js +1 -0
- package/dist/frontend/assets/haskell-Df6bDoY_.js +1 -0
- package/dist/frontend/assets/haxe-CzTSHFRz.js +1 -0
- package/dist/frontend/assets/hcl-BWvSN4gD.js +1 -0
- package/dist/frontend/assets/hjson-D5-asLiD.js +1 -0
- package/dist/frontend/assets/hlsl-D3lLCCz7.js +1 -0
- package/dist/frontend/assets/horizon-BUw7H-hv.js +1 -0
- package/dist/frontend/assets/horizon-bright-CUuTKBJd.js +1 -0
- package/dist/frontend/assets/houston-DnULxvSX.js +1 -0
- package/dist/frontend/assets/html-derivative-DlHx6ybY.js +1 -0
- package/dist/frontend/assets/html-pp8916En.js +1 -0
- package/dist/frontend/assets/http-jrhK8wxY.js +1 -0
- package/dist/frontend/assets/hurl-irOxFIW8.js +1 -0
- package/dist/frontend/assets/hxml-Bvhsp5Yf.js +1 -0
- package/dist/frontend/assets/hy-DFXneXwc.js +1 -0
- package/dist/frontend/assets/imba-DGztddWO.js +1 -0
- package/dist/frontend/assets/ini-BEwlwnbL.js +1 -0
- package/dist/frontend/assets/java-CylS5w8V.js +1 -0
- package/dist/frontend/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/frontend/assets/jinja-f2NsQr07.js +1 -0
- package/dist/frontend/assets/jison-wvAkD_A8.js +1 -0
- package/dist/frontend/assets/json-Cp-IABpG.js +1 -0
- package/dist/frontend/assets/json5-C9tS-k6U.js +1 -0
- package/dist/frontend/assets/jsonc-Des-eS-w.js +1 -0
- package/dist/frontend/assets/jsonl-DcaNXYhu.js +1 -0
- package/dist/frontend/assets/jsonnet-DFQXde-d.js +1 -0
- package/dist/frontend/assets/jssm-C2t-YnRu.js +1 -0
- package/dist/frontend/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/frontend/assets/julia-CxzCAyBv.js +1 -0
- package/dist/frontend/assets/just-VxiPbLrw.js +1 -0
- package/dist/frontend/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- package/dist/frontend/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- package/dist/frontend/assets/kanagawa-wave-DWedfzmr.js +1 -0
- package/dist/frontend/assets/kdl-DV7GczEv.js +1 -0
- package/dist/frontend/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/frontend/assets/kusto-wEQ09or8.js +1 -0
- package/dist/frontend/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/frontend/assets/latex-CWtU0Tv5.js +1 -0
- package/dist/frontend/assets/lean-BZvkOJ9d.js +1 -0
- package/dist/frontend/assets/less-B1dDrJ26.js +1 -0
- package/dist/frontend/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/frontend/assets/liquid-C0sCDyMI.js +1 -0
- package/dist/frontend/assets/llvm-DjAJT7YJ.js +1 -0
- package/dist/frontend/assets/log-2UxHyX5q.js +1 -0
- package/dist/frontend/assets/logo-BtOb2qkB.js +1 -0
- package/dist/frontend/assets/lua-BaeVxFsk.js +1 -0
- package/dist/frontend/assets/luau-C-HG3fhB.js +1 -0
- package/dist/frontend/assets/main-CL5_Wlhv.css +32 -0
- package/dist/frontend/assets/main-Czet4Z1x.js +371 -0
- package/dist/frontend/assets/make-CHLpvVh8.js +1 -0
- package/dist/frontend/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/frontend/assets/marko-DjSrsDqO.js +1 -0
- package/dist/frontend/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/frontend/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/frontend/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/frontend/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/frontend/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/frontend/assets/matlab-D7o27uSR.js +1 -0
- package/dist/frontend/assets/mdc-DTYItulj.js +1 -0
- package/dist/frontend/assets/mdx-Cmh6b_Ma.js +1 -0
- package/dist/frontend/assets/mermaid-mWjccvbQ.js +1 -0
- package/dist/frontend/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/frontend/assets/min-light-CTRr51gU.js +1 -0
- package/dist/frontend/assets/mipsasm-CKIfxQSi.js +1 -0
- package/dist/frontend/assets/mojo-rZm6bMo-.js +1 -0
- package/dist/frontend/assets/monokai-D4h5O-jR.js +1 -0
- package/dist/frontend/assets/moonbit-_H4v1dQx.js +1 -0
- package/dist/frontend/assets/move-IF9eRakj.js +1 -0
- package/dist/frontend/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/frontend/assets/nextflow-C-mBbutL.js +1 -0
- package/dist/frontend/assets/nextflow-groovy-vE_lwT2v.js +1 -0
- package/dist/frontend/assets/nginx-BpAMiNFr.js +1 -0
- package/dist/frontend/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/frontend/assets/night-owl-light-CMTm3GFP.js +1 -0
- package/dist/frontend/assets/nim-BIad80T-.js +1 -0
- package/dist/frontend/assets/nix-CwoSXNpI.js +1 -0
- package/dist/frontend/assets/nord-Ddv68eIx.js +1 -0
- package/dist/frontend/assets/nushell-Cz2AlsmD.js +1 -0
- package/dist/frontend/assets/objective-c-DXmwc3jG.js +1 -0
- package/dist/frontend/assets/objective-cpp-CLxacb5B.js +1 -0
- package/dist/frontend/assets/ocaml-C0hk2d4L.js +1 -0
- package/dist/frontend/assets/odin-BBf5iR-q.js +1 -0
- package/dist/frontend/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/dist/frontend/assets/one-light-C3Wv6jpd.js +1 -0
- package/dist/frontend/assets/openscad-C4EeE6gA.js +1 -0
- package/dist/frontend/assets/pascal-D93ZcfNL.js +1 -0
- package/dist/frontend/assets/perl-NvoQZIq0.js +1 -0
- package/dist/frontend/assets/php-R6g_5hLQ.js +1 -0
- package/dist/frontend/assets/pkl-u5AG7uiY.js +1 -0
- package/dist/frontend/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/frontend/assets/plsql-ChMvpjG-.js +1 -0
- package/dist/frontend/assets/po-BTJTHyun.js +1 -0
- package/dist/frontend/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/frontend/assets/polar-C0HS_06l.js +1 -0
- package/dist/frontend/assets/postcss-CXtECtnM.js +1 -0
- package/dist/frontend/assets/powerquery-CEu0bR-o.js +1 -0
- package/dist/frontend/assets/powershell-Dpen1YoG.js +1 -0
- package/dist/frontend/assets/prisma-Dd19v3D-.js +1 -0
- package/dist/frontend/assets/prolog-CbFg5uaA.js +1 -0
- package/dist/frontend/assets/proto-C7zT0LnQ.js +1 -0
- package/dist/frontend/assets/pug-DKIMFp6K.js +1 -0
- package/dist/frontend/assets/puppet-BMWR74SV.js +1 -0
- package/dist/frontend/assets/purescript-CklMAg4u.js +1 -0
- package/dist/frontend/assets/python-B6aJPvgy.js +1 -0
- package/dist/frontend/assets/qml-3beO22l8.js +1 -0
- package/dist/frontend/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/frontend/assets/qss-IeuSbFQv.js +1 -0
- package/dist/frontend/assets/r-Dspwwk_N.js +1 -0
- package/dist/frontend/assets/racket-BqYA7rlc.js +1 -0
- package/dist/frontend/assets/raku-DXvB9xmW.js +1 -0
- package/dist/frontend/assets/razor-BDqjjVU7.js +1 -0
- package/dist/frontend/assets/red-bN70gL4F.js +1 -0
- package/dist/frontend/assets/reg-C-SQnVFl.js +1 -0
- package/dist/frontend/assets/regexp-CDVJQ6XC.js +1 -0
- package/dist/frontend/assets/rel-C3B-1QV4.js +1 -0
- package/dist/frontend/assets/riscv-BM1_JUlF.js +1 -0
- package/dist/frontend/assets/ron-D8l8udqQ.js +1 -0
- package/dist/frontend/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
- package/dist/frontend/assets/rose-pine-moon-D4_iv3hh.js +1 -0
- package/dist/frontend/assets/rose-pine-qdsjHGoJ.js +1 -0
- package/dist/frontend/assets/rosmsg-BJDFO7_C.js +1 -0
- package/dist/frontend/assets/rst-CRjBmOyv.js +1 -0
- package/dist/frontend/assets/ruby-Wjq7vjNf.js +1 -0
- package/dist/frontend/assets/rust-B1yitclQ.js +1 -0
- package/dist/frontend/assets/sas-cz2c8ADy.js +1 -0
- package/dist/frontend/assets/sass-Cj5Yp3dK.js +1 -0
- package/dist/frontend/assets/scala-C151Ov-r.js +1 -0
- package/dist/frontend/assets/scheme-C98Dy4si.js +1 -0
- package/dist/frontend/assets/scss-D5BDwBP9.js +1 -0
- package/dist/frontend/assets/sdbl-DVxCFoDh.js +1 -0
- package/dist/frontend/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/dist/frontend/assets/shellscript-Yzrsuije.js +1 -0
- package/dist/frontend/assets/shellsession-BADoaaVG.js +1 -0
- package/dist/frontend/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/frontend/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/frontend/assets/smalltalk-BERRCDM3.js +1 -0
- package/dist/frontend/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/frontend/assets/solarized-dark-DXbdFlpD.js +1 -0
- package/dist/frontend/assets/solarized-light-L9t79GZl.js +1 -0
- package/dist/frontend/assets/solidity-rGO070M0.js +1 -0
- package/dist/frontend/assets/soy-8wufbnw4.js +1 -0
- package/dist/frontend/assets/sparql-rVzFXLq3.js +1 -0
- package/dist/frontend/assets/splunk-BtCnVYZw.js +1 -0
- package/dist/frontend/assets/sql-BLtJtn59.js +1 -0
- package/dist/frontend/assets/ssh-config-_ykCGR6B.js +1 -0
- package/dist/frontend/assets/stata-BH5u7GGu.js +1 -0
- package/dist/frontend/assets/stylus-BEDo0Tqx.js +1 -0
- package/dist/frontend/assets/surrealql-Bq5Q-fJD.js +1 -0
- package/dist/frontend/assets/svelte-Cy7k_4gC.js +1 -0
- package/dist/frontend/assets/swift-D82vCrfD.js +1 -0
- package/dist/frontend/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/frontend/assets/system-verilog-CnnmHF94.js +1 -0
- package/dist/frontend/assets/systemd-4A_iFExJ.js +1 -0
- package/dist/frontend/assets/talonscript-CkByrt1z.js +1 -0
- package/dist/frontend/assets/tasl-QIJgUcNo.js +1 -0
- package/dist/frontend/assets/tcl-dwOrl1Do.js +1 -0
- package/dist/frontend/assets/templ-DhtptRzy.js +1 -0
- package/dist/frontend/assets/terraform-BETggiCN.js +1 -0
- package/dist/frontend/assets/tex-idrVyKtj.js +1 -0
- package/dist/frontend/assets/tokyo-night-hegEt444.js +1 -0
- package/dist/frontend/assets/toml-vGWfd6FD.js +1 -0
- package/dist/frontend/assets/ts-tags-DQrlYJgV.js +1 -0
- package/dist/frontend/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/frontend/assets/tsx-COt5Ahok.js +1 -0
- package/dist/frontend/assets/turtle-BsS91CYL.js +1 -0
- package/dist/frontend/assets/twig-xg9kU7Mw.js +1 -0
- package/dist/frontend/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/frontend/assets/typespec-CAFt9gP4.js +1 -0
- package/dist/frontend/assets/typst-DHCkPAjA.js +1 -0
- package/dist/frontend/assets/v-BcVCzyr7.js +1 -0
- package/dist/frontend/assets/vala-CsfeWuGM.js +1 -0
- package/dist/frontend/assets/vb-D17OF-Vu.js +1 -0
- package/dist/frontend/assets/verilog-BQ8w6xss.js +1 -0
- package/dist/frontend/assets/vesper-DU1UobuO.js +1 -0
- package/dist/frontend/assets/vhdl-CeAyd5Ju.js +1 -0
- package/dist/frontend/assets/viml-CJc9bBzg.js +1 -0
- package/dist/frontend/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/frontend/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/frontend/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/frontend/assets/vue-D2xRrEX4.js +1 -0
- package/dist/frontend/assets/vue-html-AaS7Mt5G.js +1 -0
- package/dist/frontend/assets/vue-vine-BoDAl6tE.js +1 -0
- package/dist/frontend/assets/vyper-CDx5xZoG.js +1 -0
- package/dist/frontend/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/frontend/assets/wasm-MzD3tlZU.js +1 -0
- package/dist/frontend/assets/wenyan-BV7otONQ.js +1 -0
- package/dist/frontend/assets/wgsl-Dx-B1_4e.js +1 -0
- package/dist/frontend/assets/wikitext-BhOHFoWU.js +1 -0
- package/dist/frontend/assets/wit-5i3qLPDT.js +1 -0
- package/dist/frontend/assets/wolfram-lXgVvXCa.js +1 -0
- package/dist/frontend/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/frontend/assets/xsl-CtQFsRM5.js +1 -0
- package/dist/frontend/assets/yaml-Buea-lGh.js +1 -0
- package/dist/frontend/assets/zenscript-DVFEvuxE.js +1 -0
- package/dist/frontend/assets/zig-VOosw3JB.js +1 -0
- package/dist/frontend/icon-192.png +0 -0
- package/dist/frontend/icon-512.png +0 -0
- package/dist/frontend/icon.svg +8 -0
- package/dist/frontend/index.html +30 -0
- package/dist/frontend/manifest.json +25 -0
- package/dist/frontend/sw.js +66 -0
- package/dist/server/agent-events.js +39 -0
- package/dist/server/analytics.js +885 -0
- package/dist/server/auth.js +65 -0
- package/dist/server/belayer/executor.js +200 -0
- package/dist/server/belayer/intake.js +27 -0
- package/dist/server/belayer/pipeline.js +97 -0
- package/dist/server/belayer/pr-lifecycle.js +69 -0
- package/dist/server/belayer/prompts.js +154 -0
- package/dist/server/belayer/types.js +23 -0
- package/dist/server/branch-linker.js +137 -0
- package/dist/server/browser-content.js +145 -0
- package/dist/server/clipboard.js +63 -0
- package/dist/server/codex-hooks-adapter.js +93 -0
- package/dist/server/config.js +325 -0
- package/dist/server/gh-routes.js +163 -0
- package/dist/server/gh.js +276 -0
- package/dist/server/git-routes.js +154 -0
- package/dist/server/git.js +694 -0
- package/dist/server/github-app.js +218 -0
- package/dist/server/github-graphql.js +178 -0
- package/dist/server/hooks.js +373 -0
- package/dist/server/index.js +1549 -0
- package/dist/server/integration-github.js +137 -0
- package/dist/server/integration-jira.js +210 -0
- package/dist/server/integration-linear.js +176 -0
- package/dist/server/logger.js +18 -0
- package/dist/server/mobile-input-pipeline.js +129 -0
- package/dist/server/opencode-relay.js +53 -0
- package/dist/server/org-dashboard.js +241 -0
- package/dist/server/output-parsers/claude-parser.js +56 -0
- package/dist/server/output-parsers/codex-parser.js +13 -0
- package/dist/server/output-parsers/index.js +14 -0
- package/dist/server/output-parsers/null-parser.js +12 -0
- package/dist/server/output-parsers/opencode-parser.js +77 -0
- package/dist/server/pty-handler.js +586 -0
- package/dist/server/push.js +84 -0
- package/dist/server/review-poller.js +237 -0
- package/dist/server/sdk-handler.js +539 -0
- package/dist/server/service.js +189 -0
- package/dist/server/sessions.js +638 -0
- package/dist/server/telemetry.js +236 -0
- package/dist/server/ticket-transitions.js +166 -0
- package/dist/server/types.js +146 -0
- package/dist/server/utils.js +23 -0
- package/dist/server/watcher.js +661 -0
- package/dist/server/webhook-manager.js +547 -0
- package/dist/server/webhooks.js +73 -0
- package/dist/server/workspace-groups.js +363 -0
- package/dist/server/workspaces.js +1207 -0
- package/dist/server/ws.js +192 -0
- package/dist/test/EmptyState.spec.js +51 -0
- package/dist/test/action-coverage.test.js +139 -0
- package/dist/test/actions/registry.test.js +59 -0
- package/dist/test/actions/shortcuts.test.js +79 -0
- package/dist/test/agent-events.test.js +151 -0
- package/dist/test/analytics.test.js +158 -0
- package/dist/test/attention.test.js +91 -0
- package/dist/test/auth.test.js +105 -0
- package/dist/test/backend-state.test.js +47 -0
- package/dist/test/belayer-executor.test.js +33 -0
- package/dist/test/belayer-intake.test.js +44 -0
- package/dist/test/belayer-pipeline.test.js +113 -0
- package/dist/test/belayer-pr-lifecycle.test.js +26 -0
- package/dist/test/belayer-prompts.test.js +60 -0
- package/dist/test/belayer-types.test.js +69 -0
- package/dist/test/bin/claude-remote-cli.js +214 -0
- package/dist/test/boot-state.test.js +133 -0
- package/dist/test/branch-lifecycle.test.js +75 -0
- package/dist/test/branch-linker.test.js +236 -0
- package/dist/test/branch-rename.test.js +45 -0
- package/dist/test/branch-watcher.test.js +115 -0
- package/dist/test/browser-cli.test.js +91 -0
- package/dist/test/browser-content.test.js +93 -0
- package/dist/test/browser-tabs-ui.test.js +39 -0
- package/dist/test/changed-files-api.test.js +140 -0
- package/dist/test/clipboard.test.js +12 -0
- package/dist/test/codex-hooks-adapter.test.js +237 -0
- package/dist/test/components/EmptyState.spec.js +51 -0
- package/dist/test/components/ErrorToast.spec.js +65 -0
- package/dist/test/components/TuiCheckbox.spec.js +120 -0
- package/dist/test/components/TuiInput.spec.js +186 -0
- package/dist/test/components/leaf-component-migration.spec.js +104 -0
- package/dist/test/config-freshness.test.js +63 -0
- package/dist/test/config.test.js +813 -0
- package/dist/test/diff-summary.test.js +98 -0
- package/dist/test/display-state.test.js +179 -0
- package/dist/test/event-message-types.test.js +32 -0
- package/dist/test/file-tree-utils.test.js +167 -0
- package/dist/test/framework-types.test.js +183 -0
- package/dist/test/frameworks-api.test.js +93 -0
- package/dist/test/frontend/src/lib/pr-state.js +114 -0
- package/dist/test/fs-browse.test.js +246 -0
- package/dist/test/fuzzy-scorer.test.js +145 -0
- package/dist/test/gh-routes.test.js +156 -0
- package/dist/test/git-changed-files.test.js +152 -0
- package/dist/test/git-routes.test.js +146 -0
- package/dist/test/git-utils.test.js +68 -0
- package/dist/test/git-watcher.test.js +110 -0
- package/dist/test/git.test.js +140 -0
- package/dist/test/github-app.test.js +455 -0
- package/dist/test/github-graphql.test.js +301 -0
- package/dist/test/greetings.test.js +83 -0
- package/dist/test/hooks-agent-event.test.js +412 -0
- package/dist/test/hooks.test.js +149 -0
- package/dist/test/integration-github.test.js +220 -0
- package/dist/test/integration-jira.test.js +238 -0
- package/dist/test/integration-linear.test.js +293 -0
- package/dist/test/mobile-input.test.js +235 -0
- package/dist/test/opencode-relay.test.js +107 -0
- package/dist/test/org-dashboard.test.js +349 -0
- package/dist/test/output-parser.test.js +217 -0
- package/dist/test/paths.test.js +32 -0
- package/dist/test/pr-state.test.js +407 -0
- package/dist/test/pr-status.test.js +82 -0
- package/dist/test/presets.test.js +242 -0
- package/dist/test/pty-handler-multi-agent.test.js +149 -0
- package/dist/test/pty-handler.test.js +146 -0
- package/dist/test/pull-requests.test.js +78 -0
- package/dist/test/review-poller.test.js +349 -0
- package/dist/test/server/analytics.js +121 -0
- package/dist/test/server/auth.js +63 -0
- package/dist/test/server/branch-linker.js +124 -0
- package/dist/test/server/clipboard.js +56 -0
- package/dist/test/server/config.js +137 -0
- package/dist/test/server/git.js +308 -0
- package/dist/test/server/hooks.js +196 -0
- package/dist/test/server/index.js +1124 -0
- package/dist/test/server/integration-github.js +117 -0
- package/dist/test/server/integration-jira.js +164 -0
- package/dist/test/server/integration-linear.js +176 -0
- package/dist/test/server/mobile-input-pipeline.js +123 -0
- package/dist/test/server/org-dashboard.js +184 -0
- package/dist/test/server/output-parsers/claude-parser.js +54 -0
- package/dist/test/server/output-parsers/codex-parser.js +13 -0
- package/dist/test/server/output-parsers/index.js +7 -0
- package/dist/test/server/pty-handler.js +310 -0
- package/dist/test/server/push.js +80 -0
- package/dist/test/server/review-poller.js +218 -0
- package/dist/test/server/service.js +169 -0
- package/dist/test/server/sessions.js +434 -0
- package/dist/test/server/ticket-transitions.js +216 -0
- package/dist/test/server/types.js +20 -0
- package/dist/test/server/utils.js +22 -0
- package/dist/test/server/watcher.js +139 -0
- package/dist/test/server/workspaces.js +657 -0
- package/dist/test/server/ws.js +152 -0
- package/dist/test/server-startup.test.js +62 -0
- package/dist/test/service.test.js +43 -0
- package/dist/test/session-analytics-api.test.js +123 -0
- package/dist/test/session-analytics.test.js +425 -0
- package/dist/test/session-intent.test.js +249 -0
- package/dist/test/sessions.test.js +1152 -0
- package/dist/test/sidebar-items.test.js +164 -0
- package/dist/test/stores/boot-state-store.test.js +165 -0
- package/dist/test/stores/sessions-logic.test.js +191 -0
- package/dist/test/stores/toasts-store.test.js +66 -0
- package/dist/test/stores/ui-store.test.js +203 -0
- package/dist/test/stores/unread-store.test.js +97 -0
- package/dist/test/telemetry-api.test.js +54 -0
- package/dist/test/telemetry-sync.test.js +68 -0
- package/dist/test/telemetry.test.js +295 -0
- package/dist/test/terminal-zoom.test.js +102 -0
- package/dist/test/test/analytics.test.js +152 -0
- package/dist/test/test/auth.test.js +95 -0
- package/dist/test/test/branch-linker.test.js +231 -0
- package/dist/test/test/branch-rename.test.js +45 -0
- package/dist/test/test/clipboard.test.js +12 -0
- package/dist/test/test/config.test.js +281 -0
- package/dist/test/test/fs-browse.test.js +202 -0
- package/dist/test/test/git.test.js +67 -0
- package/dist/test/test/hooks.test.js +139 -0
- package/dist/test/test/integration-github.test.js +203 -0
- package/dist/test/test/integration-jira.test.js +294 -0
- package/dist/test/test/integration-linear.test.js +293 -0
- package/dist/test/test/mobile-input.test.js +193 -0
- package/dist/test/test/org-dashboard.test.js +240 -0
- package/dist/test/test/output-parser.test.js +95 -0
- package/dist/test/test/paths.test.js +32 -0
- package/dist/test/test/pr-state.test.js +220 -0
- package/dist/test/test/pull-requests.test.js +67 -0
- package/dist/test/test/review-poller.test.js +235 -0
- package/dist/test/test/service.test.js +43 -0
- package/dist/test/test/sessions.test.js +750 -0
- package/dist/test/test/ticket-transitions.test.js +130 -0
- package/dist/test/test/version.test.js +34 -0
- package/dist/test/test/worktrees.test.js +256 -0
- package/dist/test/ticket-transitions.test.js +312 -0
- package/dist/test/unread.test.js +23 -0
- package/dist/test/version.test.js +34 -0
- package/dist/test/webhook-manager.test.js +484 -0
- package/dist/test/webhooks.test.js +208 -0
- package/dist/test/workspace-groups.test.js +377 -0
- package/dist/test/worktrees.test.js +531 -0
- package/package.json +88 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { test, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import { createIntegrationLinearRouter } from '../server/integration-linear.js';
|
|
5
|
+
// ─── State ───────────────────────────────────────────────────────────────────
|
|
6
|
+
let server;
|
|
7
|
+
let baseUrl;
|
|
8
|
+
// Saved before any mock replaces globalThis.fetch so test HTTP calls
|
|
9
|
+
// to the local Express server always reach it even when fetch is mocked.
|
|
10
|
+
let httpFetch;
|
|
11
|
+
let originalApiKey;
|
|
12
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
13
|
+
/** Builds a single Linear issue node as returned by the GraphQL API. */
|
|
14
|
+
function makeIssueNode(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
id: overrides.id ?? 'issue-1',
|
|
17
|
+
identifier: overrides.identifier ?? 'ENG-1',
|
|
18
|
+
title: overrides.title ?? 'Test Issue',
|
|
19
|
+
url: overrides.url ?? 'https://linear.app/team/issue/ENG-1',
|
|
20
|
+
state: overrides.stateName != null ? { name: overrides.stateName } : { name: 'In Progress' },
|
|
21
|
+
priority: overrides.priority ?? 2,
|
|
22
|
+
priorityLabel: overrides.priorityLabel ?? 'Medium',
|
|
23
|
+
cycle: (overrides.cycle !== undefined)
|
|
24
|
+
? (overrides.cycle !== null ? { name: overrides.cycle } : null)
|
|
25
|
+
: null,
|
|
26
|
+
estimate: overrides.estimate ?? null,
|
|
27
|
+
assignee: overrides.assigneeName != null ? { name: overrides.assigneeName } : null,
|
|
28
|
+
updatedAt: overrides.updatedAt ?? '2026-03-21T00:00:00Z',
|
|
29
|
+
team: overrides.teamId != null ? { id: overrides.teamId } : { id: 'team-abc' },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Builds a GraphQL issues response envelope. */
|
|
33
|
+
function makeIssuesGqlResponse(nodes) {
|
|
34
|
+
return {
|
|
35
|
+
data: {
|
|
36
|
+
viewer: {
|
|
37
|
+
assignedIssues: {
|
|
38
|
+
nodes,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Builds a GraphQL workflow states response envelope. */
|
|
45
|
+
function makeStatesGqlResponse(nodes) {
|
|
46
|
+
return {
|
|
47
|
+
data: {
|
|
48
|
+
workflowStates: {
|
|
49
|
+
nodes,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Returns a function suitable for replacing globalThis.fetch.
|
|
56
|
+
* The returned mock always resolves with a minimal Response-shaped object.
|
|
57
|
+
*/
|
|
58
|
+
function makeMockFetch(gqlBody, opts = {}) {
|
|
59
|
+
const status = opts.status ?? 200;
|
|
60
|
+
const ok = opts.ok !== undefined ? opts.ok : (status >= 200 && status < 300);
|
|
61
|
+
return (async () => ({
|
|
62
|
+
ok,
|
|
63
|
+
status,
|
|
64
|
+
json: async () => gqlBody,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
function startServer() {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const app = express();
|
|
70
|
+
app.use(express.json());
|
|
71
|
+
// configPath is unused by the Linear router at runtime; pass a dummy.
|
|
72
|
+
app.use('/integration-linear', createIntegrationLinearRouter({ configPath: '/dev/null' }));
|
|
73
|
+
server = app.listen(0, '127.0.0.1', () => {
|
|
74
|
+
const addr = server.address();
|
|
75
|
+
if (typeof addr === 'object' && addr) {
|
|
76
|
+
baseUrl = `http://127.0.0.1:${addr.port}`;
|
|
77
|
+
}
|
|
78
|
+
resolve();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function stopServer() {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
if (server)
|
|
85
|
+
server.close(() => resolve());
|
|
86
|
+
else
|
|
87
|
+
resolve();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────────────
|
|
91
|
+
// Each test gets a fresh server instance so the module-level issuesCache starts
|
|
92
|
+
// at null. This is the only reliable way to clear cache state without exposing
|
|
93
|
+
// internals, since issuesCache lives in the router closure.
|
|
94
|
+
beforeEach(async () => {
|
|
95
|
+
await startServer();
|
|
96
|
+
// Capture real fetch AFTER server starts (so baseUrl is set) but BEFORE any
|
|
97
|
+
// test mock replaces it. Used for all test-to-server HTTP calls.
|
|
98
|
+
httpFetch = globalThis.fetch;
|
|
99
|
+
originalApiKey = process.env['LINEAR_API_KEY'];
|
|
100
|
+
});
|
|
101
|
+
afterEach(async () => {
|
|
102
|
+
await stopServer();
|
|
103
|
+
globalThis.fetch = httpFetch;
|
|
104
|
+
if (originalApiKey === undefined) {
|
|
105
|
+
delete process.env['LINEAR_API_KEY'];
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
process.env['LINEAR_API_KEY'] = originalApiKey;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// ─── GET /configured ─────────────────────────────────────────────────────────
|
|
112
|
+
test('GET /configured — returns { configured: true } when LINEAR_API_KEY is set', async () => {
|
|
113
|
+
process.env['LINEAR_API_KEY'] = 'lin_test_key';
|
|
114
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/configured`);
|
|
115
|
+
assert.equal(res.ok, true);
|
|
116
|
+
const body = await res.json();
|
|
117
|
+
assert.deepEqual(body, { configured: true });
|
|
118
|
+
});
|
|
119
|
+
test('GET /configured — returns { configured: false } when LINEAR_API_KEY is not set', async () => {
|
|
120
|
+
delete process.env['LINEAR_API_KEY'];
|
|
121
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/configured`);
|
|
122
|
+
assert.equal(res.ok, true);
|
|
123
|
+
const body = await res.json();
|
|
124
|
+
assert.deepEqual(body, { configured: false });
|
|
125
|
+
});
|
|
126
|
+
// ─── GET /issues ──────────────────────────────────────────────────────────────
|
|
127
|
+
test('GET /issues — returns linear_not_configured error when API key is missing', async () => {
|
|
128
|
+
delete process.env['LINEAR_API_KEY'];
|
|
129
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/issues`);
|
|
130
|
+
assert.equal(res.ok, true);
|
|
131
|
+
const body = await res.json();
|
|
132
|
+
assert.equal(body.error, 'linear_not_configured');
|
|
133
|
+
assert.deepEqual(body.issues, []);
|
|
134
|
+
});
|
|
135
|
+
test('GET /issues — returns mapped LinearIssue[] from mocked GraphQL response', async () => {
|
|
136
|
+
process.env['LINEAR_API_KEY'] = 'lin_test_key';
|
|
137
|
+
const nodes = [
|
|
138
|
+
makeIssueNode({
|
|
139
|
+
id: 'issue-abc',
|
|
140
|
+
identifier: 'ENG-42',
|
|
141
|
+
title: 'Build something',
|
|
142
|
+
url: 'https://linear.app/team/issue/ENG-42',
|
|
143
|
+
stateName: 'In Progress',
|
|
144
|
+
priority: 1,
|
|
145
|
+
priorityLabel: 'Urgent',
|
|
146
|
+
cycle: 'Sprint 5',
|
|
147
|
+
estimate: 3,
|
|
148
|
+
assigneeName: 'Alice',
|
|
149
|
+
updatedAt: '2026-03-21T12:00:00Z',
|
|
150
|
+
teamId: 'team-xyz',
|
|
151
|
+
}),
|
|
152
|
+
];
|
|
153
|
+
globalThis.fetch = makeMockFetch(makeIssuesGqlResponse(nodes));
|
|
154
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/issues`);
|
|
155
|
+
assert.equal(res.ok, true);
|
|
156
|
+
const body = await res.json();
|
|
157
|
+
assert.equal(body.error, undefined, `Unexpected error: ${body.error}`);
|
|
158
|
+
assert.equal(body.issues.length, 1);
|
|
159
|
+
const issue = body.issues[0];
|
|
160
|
+
assert.equal(issue.id, 'issue-abc');
|
|
161
|
+
assert.equal(issue.identifier, 'ENG-42');
|
|
162
|
+
assert.equal(issue.title, 'Build something');
|
|
163
|
+
assert.equal(issue.url, 'https://linear.app/team/issue/ENG-42');
|
|
164
|
+
assert.equal(issue.state, 'In Progress');
|
|
165
|
+
assert.equal(issue.priority, 1);
|
|
166
|
+
assert.equal(issue.priorityLabel, 'Urgent');
|
|
167
|
+
assert.equal(issue.cycle, 'Sprint 5');
|
|
168
|
+
assert.equal(issue.estimate, 3);
|
|
169
|
+
assert.equal(issue.assignee, 'Alice');
|
|
170
|
+
assert.equal(issue.updatedAt, '2026-03-21T12:00:00Z');
|
|
171
|
+
assert.equal(issue.teamId, 'team-xyz');
|
|
172
|
+
});
|
|
173
|
+
test('GET /issues — caches results within TTL (fetch called only once for two requests)', async () => {
|
|
174
|
+
process.env['LINEAR_API_KEY'] = 'lin_test_key';
|
|
175
|
+
let fetchCallCount = 0;
|
|
176
|
+
const nodes = [makeIssueNode({ id: 'cached-issue', identifier: 'ENG-99' })];
|
|
177
|
+
const mockBody = makeIssuesGqlResponse(nodes);
|
|
178
|
+
globalThis.fetch = (async () => {
|
|
179
|
+
fetchCallCount++;
|
|
180
|
+
return { ok: true, status: 200, json: async () => mockBody };
|
|
181
|
+
});
|
|
182
|
+
// First request — populates cache
|
|
183
|
+
const first = await httpFetch(`${baseUrl}/integration-linear/issues`);
|
|
184
|
+
const firstBody = await first.json();
|
|
185
|
+
assert.equal(firstBody.error, undefined, `Unexpected error on first request: ${firstBody.error}`);
|
|
186
|
+
assert.equal(firstBody.issues.length, 1);
|
|
187
|
+
assert.equal(fetchCallCount, 1, 'fetch should be called once on the first request');
|
|
188
|
+
// Second request — should be served from cache, no additional fetch calls
|
|
189
|
+
const second = await httpFetch(`${baseUrl}/integration-linear/issues`);
|
|
190
|
+
const secondBody = await second.json();
|
|
191
|
+
assert.equal(secondBody.error, undefined, `Unexpected error on second request: ${secondBody.error}`);
|
|
192
|
+
assert.equal(secondBody.issues.length, 1);
|
|
193
|
+
assert.equal(fetchCallCount, 1, 'fetch should not be called again within TTL (cache hit)');
|
|
194
|
+
});
|
|
195
|
+
// ─── GET /states ──────────────────────────────────────────────────────────────
|
|
196
|
+
test('GET /states — returns workflow states from mocked GraphQL response', async () => {
|
|
197
|
+
process.env['LINEAR_API_KEY'] = 'lin_test_key';
|
|
198
|
+
const stateNodes = [
|
|
199
|
+
{ id: 'state-1', name: 'Backlog' },
|
|
200
|
+
{ id: 'state-2', name: 'In Progress' },
|
|
201
|
+
{ id: 'state-3', name: 'Done' },
|
|
202
|
+
];
|
|
203
|
+
globalThis.fetch = makeMockFetch(makeStatesGqlResponse(stateNodes));
|
|
204
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/states?teamId=team-abc`);
|
|
205
|
+
assert.equal(res.ok, true);
|
|
206
|
+
const body = await res.json();
|
|
207
|
+
assert.equal(body.error, undefined, `Unexpected error: ${body.error}`);
|
|
208
|
+
assert.equal(body.states.length, 3);
|
|
209
|
+
assert.deepEqual(body.states, stateNodes);
|
|
210
|
+
});
|
|
211
|
+
test('GET /states — returns linear_not_configured error when API key is missing', async () => {
|
|
212
|
+
delete process.env['LINEAR_API_KEY'];
|
|
213
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/states?teamId=team-abc`);
|
|
214
|
+
assert.equal(res.ok, true);
|
|
215
|
+
const body = await res.json();
|
|
216
|
+
assert.equal(body.error, 'linear_not_configured');
|
|
217
|
+
assert.deepEqual(body.states, []);
|
|
218
|
+
});
|
|
219
|
+
test('GET /states — returns 400 missing_team_id when teamId query param is absent', async () => {
|
|
220
|
+
process.env['LINEAR_API_KEY'] = 'lin_test_key';
|
|
221
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/states`);
|
|
222
|
+
assert.equal(res.status, 400);
|
|
223
|
+
const body = await res.json();
|
|
224
|
+
assert.equal(body.error, 'missing_team_id');
|
|
225
|
+
});
|
|
226
|
+
// ─── Error handling ───────────────────────────────────────────────────────────
|
|
227
|
+
test('auth failure (HTTP 401) returns linear_auth_failed for /issues', async () => {
|
|
228
|
+
process.env['LINEAR_API_KEY'] = 'lin_bad_key';
|
|
229
|
+
globalThis.fetch = makeMockFetch({}, { status: 401, ok: false });
|
|
230
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/issues`);
|
|
231
|
+
assert.equal(res.ok, true);
|
|
232
|
+
const body = await res.json();
|
|
233
|
+
assert.equal(body.error, 'linear_auth_failed');
|
|
234
|
+
assert.deepEqual(body.issues, []);
|
|
235
|
+
});
|
|
236
|
+
test('auth failure (HTTP 403) returns linear_auth_failed for /issues', async () => {
|
|
237
|
+
process.env['LINEAR_API_KEY'] = 'lin_bad_key';
|
|
238
|
+
globalThis.fetch = makeMockFetch({}, { status: 403, ok: false });
|
|
239
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/issues`);
|
|
240
|
+
assert.equal(res.ok, true);
|
|
241
|
+
const body = await res.json();
|
|
242
|
+
assert.equal(body.error, 'linear_auth_failed');
|
|
243
|
+
assert.deepEqual(body.issues, []);
|
|
244
|
+
});
|
|
245
|
+
test('non-ok response (HTTP 500) returns linear_fetch_failed for /issues', async () => {
|
|
246
|
+
process.env['LINEAR_API_KEY'] = 'lin_test_key';
|
|
247
|
+
globalThis.fetch = makeMockFetch({}, { status: 500, ok: false });
|
|
248
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/issues`);
|
|
249
|
+
assert.equal(res.ok, true);
|
|
250
|
+
const body = await res.json();
|
|
251
|
+
assert.equal(body.error, 'linear_fetch_failed');
|
|
252
|
+
assert.deepEqual(body.issues, []);
|
|
253
|
+
});
|
|
254
|
+
test('GraphQL-level authentication error returns linear_auth_failed', async () => {
|
|
255
|
+
process.env['LINEAR_API_KEY'] = 'lin_test_key';
|
|
256
|
+
const gqlAuthError = {
|
|
257
|
+
errors: [{ extensions: { type: 'authentication' } }],
|
|
258
|
+
data: null,
|
|
259
|
+
};
|
|
260
|
+
globalThis.fetch = makeMockFetch(gqlAuthError);
|
|
261
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/issues`);
|
|
262
|
+
assert.equal(res.ok, true);
|
|
263
|
+
const body = await res.json();
|
|
264
|
+
assert.equal(body.error, 'linear_auth_failed');
|
|
265
|
+
assert.deepEqual(body.issues, []);
|
|
266
|
+
});
|
|
267
|
+
test('network error (fetch throws) returns linear_fetch_failed for /issues', async () => {
|
|
268
|
+
process.env['LINEAR_API_KEY'] = 'lin_test_key';
|
|
269
|
+
globalThis.fetch = (async () => { throw new Error('Network failure'); });
|
|
270
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/issues`);
|
|
271
|
+
assert.equal(res.ok, true);
|
|
272
|
+
const body = await res.json();
|
|
273
|
+
assert.equal(body.error, 'linear_fetch_failed');
|
|
274
|
+
assert.deepEqual(body.issues, []);
|
|
275
|
+
});
|
|
276
|
+
test('auth failure (HTTP 401) returns linear_auth_failed for /states', async () => {
|
|
277
|
+
process.env['LINEAR_API_KEY'] = 'lin_bad_key';
|
|
278
|
+
globalThis.fetch = makeMockFetch({}, { status: 401, ok: false });
|
|
279
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/states?teamId=team-abc`);
|
|
280
|
+
assert.equal(res.ok, true);
|
|
281
|
+
const body = await res.json();
|
|
282
|
+
assert.equal(body.error, 'linear_auth_failed');
|
|
283
|
+
assert.deepEqual(body.states, []);
|
|
284
|
+
});
|
|
285
|
+
test('non-ok response (HTTP 500) returns linear_fetch_failed for /states', async () => {
|
|
286
|
+
process.env['LINEAR_API_KEY'] = 'lin_test_key';
|
|
287
|
+
globalThis.fetch = makeMockFetch({}, { status: 500, ok: false });
|
|
288
|
+
const res = await httpFetch(`${baseUrl}/integration-linear/states?teamId=team-abc`);
|
|
289
|
+
assert.equal(res.ok, true);
|
|
290
|
+
const body = await res.json();
|
|
291
|
+
assert.equal(body.error, 'linear_fetch_failed');
|
|
292
|
+
assert.deepEqual(body.states, []);
|
|
293
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { processIntent } from '../server/mobile-input-pipeline.js';
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const FIXTURES_DIR = join(__dirname, '..', '..', 'test', 'fixtures', 'mobile-input');
|
|
9
|
+
function loadFixture(filename) {
|
|
10
|
+
const raw = readFileSync(join(FIXTURES_DIR, filename), 'utf-8');
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
}
|
|
13
|
+
function replayFixture(fixture) {
|
|
14
|
+
let totalPayload = '';
|
|
15
|
+
for (const step of fixture.events) {
|
|
16
|
+
const intent = {
|
|
17
|
+
type: step.inputType,
|
|
18
|
+
data: step.data,
|
|
19
|
+
rangeStart: step.rangeStart,
|
|
20
|
+
rangeEnd: step.rangeEnd,
|
|
21
|
+
valueBefore: step.valueBefore,
|
|
22
|
+
cursorBefore: step.cursorBefore,
|
|
23
|
+
};
|
|
24
|
+
const result = processIntent(intent, step.valueAfter);
|
|
25
|
+
totalPayload += result.payload;
|
|
26
|
+
}
|
|
27
|
+
return totalPayload;
|
|
28
|
+
}
|
|
29
|
+
function assertReplacementNotLost(payload, expectedReplacement, fixtureName) {
|
|
30
|
+
assert.ok(payload.includes(expectedReplacement), `[${fixtureName}] Payload contains only backspaces — replacement text "${expectedReplacement}" was lost. Got: ${JSON.stringify(payload)}`);
|
|
31
|
+
}
|
|
32
|
+
// ── Fixture replay tests ─────────────────────────────────────────────
|
|
33
|
+
describe('mobile-input-pipeline: fixture replay', () => {
|
|
34
|
+
const fixtureFiles = readdirSync(FIXTURES_DIR).filter((f) => f.endsWith('.json'));
|
|
35
|
+
for (const file of fixtureFiles) {
|
|
36
|
+
const fixture = loadFixture(file);
|
|
37
|
+
it(`${fixture.name}: ${fixture.description}`, () => {
|
|
38
|
+
const actualPayload = replayFixture(fixture);
|
|
39
|
+
assert.strictEqual(actualPayload, fixture.expectedPayload, `Payload mismatch for fixture "${fixture.name}". ` +
|
|
40
|
+
`Expected: ${JSON.stringify(fixture.expectedPayload)}, ` +
|
|
41
|
+
`Got: ${JSON.stringify(actualPayload)}`);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
// ── Autocorrect invariant tests ──────────────────────────────────────
|
|
46
|
+
describe('mobile-input-pipeline: autocorrect always includes replacement text', () => {
|
|
47
|
+
it('Gboard range replacement includes replacement text', () => {
|
|
48
|
+
const fixture = loadFixture('gboard-autocorrect-range.json');
|
|
49
|
+
const payload = replayFixture(fixture);
|
|
50
|
+
assertReplacementNotLost(payload, 'the', fixture.name);
|
|
51
|
+
});
|
|
52
|
+
it('Gboard cursor-0 recovery includes replacement text', () => {
|
|
53
|
+
const fixture = loadFixture('gboard-autocorrect-cursor0.json');
|
|
54
|
+
const payload = replayFixture(fixture);
|
|
55
|
+
assertReplacementNotLost(payload, 'the', fixture.name);
|
|
56
|
+
});
|
|
57
|
+
it('iOS insertReplacementText includes replacement text', () => {
|
|
58
|
+
const fixture = loadFixture('ios-replacement-text.json');
|
|
59
|
+
const payload = replayFixture(fixture);
|
|
60
|
+
assertReplacementNotLost(payload, 'the', fixture.name);
|
|
61
|
+
});
|
|
62
|
+
it('multi-word buffer: only target word deleted, replacement inserted', () => {
|
|
63
|
+
const fixture = loadFixture('gboard-autocorrect-multi-word.json');
|
|
64
|
+
const payload = replayFixture(fixture);
|
|
65
|
+
assertReplacementNotLost(payload, 'the', fixture.name);
|
|
66
|
+
const backspaceCount = (payload.match(/\x7f/g) ?? []).length;
|
|
67
|
+
assert.strictEqual(backspaceCount, 3, `Expected 3 backspaces (for "teh") but got ${backspaceCount} — ` +
|
|
68
|
+
`pipeline is deleting more than the target word`);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
// ── processIntent unit tests ─────────────────────────────────────────
|
|
72
|
+
describe('mobile-input-pipeline: processIntent', () => {
|
|
73
|
+
it('normal character insertion sends data directly', () => {
|
|
74
|
+
const result = processIntent({
|
|
75
|
+
type: 'insertText',
|
|
76
|
+
data: 'a',
|
|
77
|
+
rangeStart: 5,
|
|
78
|
+
rangeEnd: 5,
|
|
79
|
+
valueBefore: 'hello',
|
|
80
|
+
cursorBefore: 5,
|
|
81
|
+
}, 'helloa');
|
|
82
|
+
assert.strictEqual(result.payload, 'a');
|
|
83
|
+
assert.strictEqual(result.newInputValue, undefined);
|
|
84
|
+
});
|
|
85
|
+
it('autocorrect with range sends backspaces + replacement', () => {
|
|
86
|
+
const result = processIntent({
|
|
87
|
+
type: 'insertText',
|
|
88
|
+
data: 'the',
|
|
89
|
+
rangeStart: 0,
|
|
90
|
+
rangeEnd: 3,
|
|
91
|
+
valueBefore: 'teh',
|
|
92
|
+
cursorBefore: 3,
|
|
93
|
+
}, 'the');
|
|
94
|
+
assert.strictEqual(result.payload, '\x7f\x7f\x7fthe');
|
|
95
|
+
});
|
|
96
|
+
it('cursor-0 recovery: single word buffer', () => {
|
|
97
|
+
const result = processIntent({
|
|
98
|
+
type: 'insertText',
|
|
99
|
+
data: 'the',
|
|
100
|
+
rangeStart: null,
|
|
101
|
+
rangeEnd: null,
|
|
102
|
+
valueBefore: 'teh',
|
|
103
|
+
cursorBefore: 0,
|
|
104
|
+
}, 'theteh');
|
|
105
|
+
assert.strictEqual(result.payload, '\x7f\x7f\x7fthe');
|
|
106
|
+
assert.strictEqual(result.newInputValue, 'the');
|
|
107
|
+
});
|
|
108
|
+
it('cursor-0 recovery: multi-word buffer only deletes last word', () => {
|
|
109
|
+
const result = processIntent({
|
|
110
|
+
type: 'insertText',
|
|
111
|
+
data: 'mobile ',
|
|
112
|
+
rangeStart: null,
|
|
113
|
+
rangeEnd: null,
|
|
114
|
+
valueBefore: 'and mkbijf',
|
|
115
|
+
cursorBefore: 0,
|
|
116
|
+
}, 'mobile and mkbijf');
|
|
117
|
+
assert.strictEqual(result.payload, '\x7f\x7f\x7f\x7f\x7f\x7fmobile ');
|
|
118
|
+
assert.strictEqual(result.newInputValue, 'and mobile ');
|
|
119
|
+
});
|
|
120
|
+
it('cursor-0 recovery: suffix-only data (data[0] !== firstChar)', () => {
|
|
121
|
+
// Gboard sends suffix "esting " instead of full word "testing "
|
|
122
|
+
// because firstChar "t" was kept and data starts with "e"
|
|
123
|
+
const result = processIntent({
|
|
124
|
+
type: 'insertText',
|
|
125
|
+
data: 'esting ',
|
|
126
|
+
rangeStart: null,
|
|
127
|
+
rangeEnd: null,
|
|
128
|
+
valueBefore: 'tsestin',
|
|
129
|
+
cursorBefore: 0,
|
|
130
|
+
}, 'esting tsestin');
|
|
131
|
+
// data[0]='e' !== firstChar='t' → suffix mode: "t" + "esting " = "testing "
|
|
132
|
+
assert.strictEqual(result.payload, '\x7f\x7f\x7f\x7f\x7f\x7f\x7ftesting ');
|
|
133
|
+
assert.strictEqual(result.newInputValue, 'testing ');
|
|
134
|
+
});
|
|
135
|
+
it('cursor-0 recovery: trailing space means nothing to autocorrect', () => {
|
|
136
|
+
const result = processIntent({
|
|
137
|
+
type: 'insertText',
|
|
138
|
+
data: 'the ',
|
|
139
|
+
rangeStart: null,
|
|
140
|
+
rangeEnd: null,
|
|
141
|
+
valueBefore: 'hello ',
|
|
142
|
+
cursorBefore: 0,
|
|
143
|
+
}, 'the hello ');
|
|
144
|
+
assert.strictEqual(result.payload, '');
|
|
145
|
+
assert.strictEqual(result.newInputValue, 'hello ');
|
|
146
|
+
});
|
|
147
|
+
it('deleteContentBackward with range', () => {
|
|
148
|
+
const result = processIntent({
|
|
149
|
+
type: 'deleteContentBackward',
|
|
150
|
+
data: null,
|
|
151
|
+
rangeStart: 4,
|
|
152
|
+
rangeEnd: 5,
|
|
153
|
+
valueBefore: 'hello',
|
|
154
|
+
cursorBefore: 5,
|
|
155
|
+
}, 'hell');
|
|
156
|
+
assert.strictEqual(result.payload, '\x7f');
|
|
157
|
+
});
|
|
158
|
+
it('deleteWordBackward with range sends correct backspace count', () => {
|
|
159
|
+
const result = processIntent({
|
|
160
|
+
type: 'deleteWordBackward',
|
|
161
|
+
data: null,
|
|
162
|
+
rangeStart: 6,
|
|
163
|
+
rangeEnd: 11,
|
|
164
|
+
valueBefore: 'hello world',
|
|
165
|
+
cursorBefore: 11,
|
|
166
|
+
}, 'hello ');
|
|
167
|
+
assert.strictEqual(result.payload, '\x7f\x7f\x7f\x7f\x7f');
|
|
168
|
+
});
|
|
169
|
+
it('deleteContentBackward without range falls back to diff', () => {
|
|
170
|
+
const result = processIntent({
|
|
171
|
+
type: 'deleteContentBackward',
|
|
172
|
+
data: null,
|
|
173
|
+
rangeStart: null,
|
|
174
|
+
rangeEnd: null,
|
|
175
|
+
valueBefore: 'hello',
|
|
176
|
+
cursorBefore: 5,
|
|
177
|
+
}, 'hell');
|
|
178
|
+
assert.strictEqual(result.payload, '\x7f');
|
|
179
|
+
});
|
|
180
|
+
it('insertReplacementText sends backspaces + replacement', () => {
|
|
181
|
+
const result = processIntent({
|
|
182
|
+
type: 'insertReplacementText',
|
|
183
|
+
data: 'the',
|
|
184
|
+
rangeStart: 0,
|
|
185
|
+
rangeEnd: 3,
|
|
186
|
+
valueBefore: 'teh',
|
|
187
|
+
cursorBefore: 3,
|
|
188
|
+
}, 'the');
|
|
189
|
+
assert.strictEqual(result.payload, '\x7f\x7f\x7fthe');
|
|
190
|
+
});
|
|
191
|
+
it('insertFromPaste uses diff to extract pasted text', () => {
|
|
192
|
+
const result = processIntent({
|
|
193
|
+
type: 'insertFromPaste',
|
|
194
|
+
data: null,
|
|
195
|
+
rangeStart: null,
|
|
196
|
+
rangeEnd: null,
|
|
197
|
+
valueBefore: 'hello',
|
|
198
|
+
cursorBefore: 5,
|
|
199
|
+
}, 'hello world');
|
|
200
|
+
assert.strictEqual(result.payload, ' world');
|
|
201
|
+
});
|
|
202
|
+
it('unknown inputType falls back to diff', () => {
|
|
203
|
+
const result = processIntent({
|
|
204
|
+
type: 'insertFromYank',
|
|
205
|
+
data: null,
|
|
206
|
+
rangeStart: null,
|
|
207
|
+
rangeEnd: null,
|
|
208
|
+
valueBefore: 'hllo',
|
|
209
|
+
cursorBefore: 1,
|
|
210
|
+
}, 'hello');
|
|
211
|
+
assert.strictEqual(result.payload, '\x7f\x7f\x7fello');
|
|
212
|
+
});
|
|
213
|
+
it('empty payload for no-op diff', () => {
|
|
214
|
+
const result = processIntent({
|
|
215
|
+
type: 'insertText',
|
|
216
|
+
data: null,
|
|
217
|
+
rangeStart: null,
|
|
218
|
+
rangeEnd: null,
|
|
219
|
+
valueBefore: 'hello',
|
|
220
|
+
cursorBefore: 5,
|
|
221
|
+
}, 'hello');
|
|
222
|
+
assert.strictEqual(result.payload, '');
|
|
223
|
+
});
|
|
224
|
+
it('handles emoji codepoints correctly in autocorrect range', () => {
|
|
225
|
+
const result = processIntent({
|
|
226
|
+
type: 'insertText',
|
|
227
|
+
data: 'smile',
|
|
228
|
+
rangeStart: 0,
|
|
229
|
+
rangeEnd: 2,
|
|
230
|
+
valueBefore: '😊',
|
|
231
|
+
cursorBefore: 2,
|
|
232
|
+
}, 'smile');
|
|
233
|
+
assert.strictEqual(result.payload, '\x7fsmile');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { installOpencodeRelayPlugin, RELAY_PLUGIN_SOURCE, } from '../server/opencode-relay.js';
|
|
7
|
+
let tmpDir;
|
|
8
|
+
before(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'crc-opencode-relay-test-'));
|
|
10
|
+
});
|
|
11
|
+
after(() => {
|
|
12
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
describe('RELAY_PLUGIN_SOURCE', () => {
|
|
15
|
+
it('contains expected opencode hook event names', () => {
|
|
16
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('session.created'), 'should include session.created');
|
|
17
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('session.idle'), 'should include session.idle');
|
|
18
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('session.status'), 'should include session.status');
|
|
19
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('session.error'), 'should include session.error');
|
|
20
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('permission.asked'), 'should include permission.asked');
|
|
21
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('permission.replied'), 'should include permission.replied');
|
|
22
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('tool.execute.before'), 'should include tool.execute.before');
|
|
23
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('tool.execute.after'), 'should include tool.execute.after');
|
|
24
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('message.updated'), 'should include message.updated');
|
|
25
|
+
});
|
|
26
|
+
it('reads CRC_RELAY_URL from env', () => {
|
|
27
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('CRC_RELAY_URL'), 'should read CRC_RELAY_URL from env');
|
|
28
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('process.env.CRC_RELAY_URL'), 'should use process.env.CRC_RELAY_URL');
|
|
29
|
+
});
|
|
30
|
+
it('reads CRC_SESSION_ID from env', () => {
|
|
31
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('CRC_SESSION_ID'), 'should read CRC_SESSION_ID from env');
|
|
32
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('process.env.CRC_SESSION_ID'), 'should use process.env.CRC_SESSION_ID');
|
|
33
|
+
});
|
|
34
|
+
it('reads CRC_RELAY_TOKEN from env', () => {
|
|
35
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('CRC_RELAY_TOKEN'), 'should read CRC_RELAY_TOKEN from env');
|
|
36
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('process.env.CRC_RELAY_TOKEN'), 'should use process.env.CRC_RELAY_TOKEN');
|
|
37
|
+
});
|
|
38
|
+
it('relays to /hooks/agent-event endpoint', () => {
|
|
39
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('/hooks/agent-event'), 'should POST to /hooks/agent-event');
|
|
40
|
+
});
|
|
41
|
+
it('uses POST method for relay', () => {
|
|
42
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes("method: 'POST'"), 'should use POST method');
|
|
43
|
+
});
|
|
44
|
+
it('returns early when env vars are missing', () => {
|
|
45
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('if (!relayUrl || !sessionId || !token) return {};'), 'should return empty object when env vars are missing');
|
|
46
|
+
});
|
|
47
|
+
it('includes sessionId and token in request body', () => {
|
|
48
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('sessionId'), 'should include sessionId in body');
|
|
49
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('token'), 'should include token in body');
|
|
50
|
+
});
|
|
51
|
+
it('includes eventType and timestamp in request body', () => {
|
|
52
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('eventType'), 'should include eventType in body');
|
|
53
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('timestamp'), 'should include timestamp in body');
|
|
54
|
+
});
|
|
55
|
+
it('is a non-empty string', () => {
|
|
56
|
+
assert.ok(typeof RELAY_PLUGIN_SOURCE === 'string', 'should be a string');
|
|
57
|
+
assert.ok(RELAY_PLUGIN_SOURCE.length > 0, 'should not be empty');
|
|
58
|
+
});
|
|
59
|
+
it('exports a default async function', () => {
|
|
60
|
+
assert.ok(RELAY_PLUGIN_SOURCE.includes('export default async'), 'should export a default async function');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('installOpencodeRelayPlugin', () => {
|
|
64
|
+
it('writes the plugin file to the specified directory', () => {
|
|
65
|
+
const pluginDir = path.join(tmpDir, 'write-test');
|
|
66
|
+
const pluginPath = installOpencodeRelayPlugin(pluginDir);
|
|
67
|
+
assert.ok(fs.existsSync(pluginPath), 'plugin file should exist');
|
|
68
|
+
assert.strictEqual(path.basename(pluginPath), 'crc-relay.ts', 'file should be named crc-relay.ts');
|
|
69
|
+
});
|
|
70
|
+
it('returns the path to the written plugin file', () => {
|
|
71
|
+
const pluginDir = path.join(tmpDir, 'return-path-test');
|
|
72
|
+
const pluginPath = installOpencodeRelayPlugin(pluginDir);
|
|
73
|
+
const expectedPath = path.join(pluginDir, 'crc-relay.ts');
|
|
74
|
+
assert.strictEqual(pluginPath, expectedPath, 'should return path to crc-relay.ts in pluginDir');
|
|
75
|
+
});
|
|
76
|
+
it('creates the directory if it does not exist', () => {
|
|
77
|
+
const pluginDir = path.join(tmpDir, 'nested', 'dirs', 'plugins');
|
|
78
|
+
assert.ok(!fs.existsSync(pluginDir), 'directory should not exist yet');
|
|
79
|
+
installOpencodeRelayPlugin(pluginDir);
|
|
80
|
+
assert.ok(fs.existsSync(pluginDir), 'directory should be created');
|
|
81
|
+
});
|
|
82
|
+
it('writes exactly RELAY_PLUGIN_SOURCE as the file content', () => {
|
|
83
|
+
const pluginDir = path.join(tmpDir, 'content-test');
|
|
84
|
+
const pluginPath = installOpencodeRelayPlugin(pluginDir);
|
|
85
|
+
const content = fs.readFileSync(pluginPath, 'utf-8');
|
|
86
|
+
assert.strictEqual(content, RELAY_PLUGIN_SOURCE, 'file content should match RELAY_PLUGIN_SOURCE');
|
|
87
|
+
});
|
|
88
|
+
it('is idempotent — write twice, file still has valid content', () => {
|
|
89
|
+
const pluginDir = path.join(tmpDir, 'idempotent-test');
|
|
90
|
+
installOpencodeRelayPlugin(pluginDir);
|
|
91
|
+
installOpencodeRelayPlugin(pluginDir);
|
|
92
|
+
const pluginPath = path.join(pluginDir, 'crc-relay.ts');
|
|
93
|
+
const content = fs.readFileSync(pluginPath, 'utf-8');
|
|
94
|
+
assert.strictEqual(content, RELAY_PLUGIN_SOURCE, 'file should still have valid content after second write');
|
|
95
|
+
});
|
|
96
|
+
it('default path includes opencode/plugins in home dir when no arg given', () => {
|
|
97
|
+
// We test the default path logic by inspecting the function signature behavior.
|
|
98
|
+
// We cannot call with no arg since it would write to the real ~/.config/opencode/plugins,
|
|
99
|
+
// so instead we verify that pluginDir parameter defaults correctly by checking the source.
|
|
100
|
+
// The actual installOpencodeRelayPlugin(pluginDir) overload is tested above.
|
|
101
|
+
// Just verify the function is callable and returns a string path.
|
|
102
|
+
const pluginDir = path.join(tmpDir, 'default-path-test');
|
|
103
|
+
const result = installOpencodeRelayPlugin(pluginDir);
|
|
104
|
+
assert.ok(typeof result === 'string', 'should return a string path');
|
|
105
|
+
assert.ok(result.endsWith('crc-relay.ts'), 'returned path should end with crc-relay.ts');
|
|
106
|
+
});
|
|
107
|
+
});
|