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,236 @@
|
|
|
1
|
+
import { test, 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 express from 'express';
|
|
7
|
+
import { createBranchLinkerRouter, invalidateBranchLinkerCache, } from '../server/branch-linker.js';
|
|
8
|
+
import { saveConfig, DEFAULTS } from '../server/config.js';
|
|
9
|
+
let tmpDir;
|
|
10
|
+
let configPath;
|
|
11
|
+
let server;
|
|
12
|
+
let baseUrl;
|
|
13
|
+
const WORKSPACE_PATH_A = '/fake/workspace/repo-a';
|
|
14
|
+
const WORKSPACE_PATH_B = '/fake/workspace/repo-b';
|
|
15
|
+
/**
|
|
16
|
+
* Creates a mock execAsync that returns configured branch lists per cwd.
|
|
17
|
+
* - `git branch -a` → returns newline-joined branch names or throws
|
|
18
|
+
*/
|
|
19
|
+
function makeMockExec(opts) {
|
|
20
|
+
return async (cmd, args, options) => {
|
|
21
|
+
const command = cmd;
|
|
22
|
+
const argv = args;
|
|
23
|
+
const cwd = options.cwd ?? '';
|
|
24
|
+
if (command === 'git' && argv[0] === 'branch') {
|
|
25
|
+
if (opts.errorByPath?.[cwd])
|
|
26
|
+
throw opts.errorByPath[cwd];
|
|
27
|
+
const branches = opts.branchesByPath?.[cwd] ?? [];
|
|
28
|
+
return { stdout: branches.join('\n') + '\n', stderr: '' };
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Unexpected exec call: ${command} ${argv.join(' ')}`);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function startServer(execAsyncFn, getActiveBranchNames) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const app = express();
|
|
36
|
+
app.use(express.json());
|
|
37
|
+
const deps = {
|
|
38
|
+
configPath,
|
|
39
|
+
execAsync: execAsyncFn,
|
|
40
|
+
...(getActiveBranchNames ? { getActiveBranchNames } : {}),
|
|
41
|
+
};
|
|
42
|
+
app.use('/branch-linker', createBranchLinkerRouter(deps));
|
|
43
|
+
server = app.listen(0, '127.0.0.1', () => {
|
|
44
|
+
const addr = server.address();
|
|
45
|
+
if (typeof addr === 'object' && addr) {
|
|
46
|
+
baseUrl = `http://127.0.0.1:${addr.port}`;
|
|
47
|
+
}
|
|
48
|
+
resolve();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function stopServer() {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
if (server)
|
|
55
|
+
server.close(() => resolve());
|
|
56
|
+
else
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async function getLinks() {
|
|
61
|
+
const res = await fetch(`${baseUrl}/branch-linker/links`);
|
|
62
|
+
return res.json();
|
|
63
|
+
}
|
|
64
|
+
before(() => {
|
|
65
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'branch-linker-test-'));
|
|
66
|
+
configPath = path.join(tmpDir, 'config.json');
|
|
67
|
+
// Clear any module-level cache before test suite runs
|
|
68
|
+
invalidateBranchLinkerCache();
|
|
69
|
+
});
|
|
70
|
+
after(async () => {
|
|
71
|
+
await stopServer();
|
|
72
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
test('extracts Jira ticket IDs from branch names', async () => {
|
|
75
|
+
await stopServer();
|
|
76
|
+
invalidateBranchLinkerCache();
|
|
77
|
+
saveConfig(configPath, {
|
|
78
|
+
...DEFAULTS,
|
|
79
|
+
repos: [WORKSPACE_PATH_A],
|
|
80
|
+
});
|
|
81
|
+
const exec = makeMockExec({
|
|
82
|
+
branchesByPath: {
|
|
83
|
+
[WORKSPACE_PATH_A]: ['dy/fix/ACME-123-auth', 'main'],
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
await startServer(exec);
|
|
87
|
+
const data = await getLinks();
|
|
88
|
+
assert.ok('ACME-123' in data, 'Should extract ACME-123 from branch name');
|
|
89
|
+
const links = data['ACME-123'];
|
|
90
|
+
assert.equal(links.length, 1);
|
|
91
|
+
assert.equal(links[0].branchName, 'dy/fix/ACME-123-auth');
|
|
92
|
+
assert.equal(links[0].repoPath, WORKSPACE_PATH_A);
|
|
93
|
+
assert.equal(links[0].repoName, 'repo-a');
|
|
94
|
+
});
|
|
95
|
+
test('extracts GH issue IDs from gh-N branches', async () => {
|
|
96
|
+
await stopServer();
|
|
97
|
+
invalidateBranchLinkerCache();
|
|
98
|
+
saveConfig(configPath, {
|
|
99
|
+
...DEFAULTS,
|
|
100
|
+
repos: [WORKSPACE_PATH_A],
|
|
101
|
+
});
|
|
102
|
+
const exec = makeMockExec({
|
|
103
|
+
branchesByPath: {
|
|
104
|
+
// Use a branch that only the GH regex matches (no embedded uppercase-only letters)
|
|
105
|
+
// to get a single clean GH-42 link. The Jira regex (/[A-Z]{2,}-\d+/gi) would also
|
|
106
|
+
// match 'gh-42' since the flag is case-insensitive, so we use a prefix that isolates
|
|
107
|
+
// the GH regex match by starting with 'gh-' at the very start of the branch name.
|
|
108
|
+
[WORKSPACE_PATH_A]: ['gh-42-login-fix'],
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
await startServer(exec);
|
|
112
|
+
const data = await getLinks();
|
|
113
|
+
assert.ok('GH-42' in data, 'Should extract GH-42 from branch name');
|
|
114
|
+
const links = data['GH-42'];
|
|
115
|
+
// Only the GH regex matches this branch — the Jira regex explicitly excludes 'GH'
|
|
116
|
+
// to avoid double-matching. Verify all links point to the correct branch and repo.
|
|
117
|
+
assert.ok(links.length >= 1, 'Should have at least one GH-42 link');
|
|
118
|
+
assert.ok(links.every((l) => l.branchName === 'gh-42-login-fix'), 'All links should reference the correct branch');
|
|
119
|
+
assert.ok(links.every((l) => l.repoPath === WORKSPACE_PATH_A), 'All links should reference the correct repo');
|
|
120
|
+
});
|
|
121
|
+
test('same ticket in two repos yields array of 2 BranchLinks', async () => {
|
|
122
|
+
await stopServer();
|
|
123
|
+
invalidateBranchLinkerCache();
|
|
124
|
+
saveConfig(configPath, {
|
|
125
|
+
...DEFAULTS,
|
|
126
|
+
repos: [WORKSPACE_PATH_A, WORKSPACE_PATH_B],
|
|
127
|
+
});
|
|
128
|
+
const exec = makeMockExec({
|
|
129
|
+
branchesByPath: {
|
|
130
|
+
[WORKSPACE_PATH_A]: ['feature/PROJ-99-payment'],
|
|
131
|
+
[WORKSPACE_PATH_B]: ['bugfix/PROJ-99-payment-fix'],
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
await startServer(exec);
|
|
135
|
+
const data = await getLinks();
|
|
136
|
+
assert.ok('PROJ-99' in data, 'Should have PROJ-99 key');
|
|
137
|
+
const links = data['PROJ-99'];
|
|
138
|
+
assert.equal(links.length, 2, 'Should have 2 BranchLinks for the same ticket across 2 repos');
|
|
139
|
+
const repoPaths = links.map((l) => l.repoPath).sort();
|
|
140
|
+
assert.deepEqual(repoPaths, [WORKSPACE_PATH_A, WORKSPACE_PATH_B].sort());
|
|
141
|
+
});
|
|
142
|
+
test('ignores branches without ticket IDs', async () => {
|
|
143
|
+
await stopServer();
|
|
144
|
+
invalidateBranchLinkerCache();
|
|
145
|
+
saveConfig(configPath, {
|
|
146
|
+
...DEFAULTS,
|
|
147
|
+
repos: [WORKSPACE_PATH_A],
|
|
148
|
+
});
|
|
149
|
+
const exec = makeMockExec({
|
|
150
|
+
branchesByPath: {
|
|
151
|
+
[WORKSPACE_PATH_A]: [
|
|
152
|
+
'main',
|
|
153
|
+
'develop',
|
|
154
|
+
'chore/cleanup',
|
|
155
|
+
'feature/new-ui',
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
await startServer(exec);
|
|
160
|
+
const data = await getLinks();
|
|
161
|
+
assert.equal(Object.keys(data).length, 0, 'Plain branches should produce no ticket links');
|
|
162
|
+
});
|
|
163
|
+
test('hasActiveSession true when branch is in active set', async () => {
|
|
164
|
+
await stopServer();
|
|
165
|
+
invalidateBranchLinkerCache();
|
|
166
|
+
saveConfig(configPath, {
|
|
167
|
+
...DEFAULTS,
|
|
168
|
+
repos: [WORKSPACE_PATH_A],
|
|
169
|
+
});
|
|
170
|
+
const activeBranch = 'feature/ACTIVE-1-work';
|
|
171
|
+
const exec = makeMockExec({
|
|
172
|
+
branchesByPath: {
|
|
173
|
+
[WORKSPACE_PATH_A]: [activeBranch, 'feature/INACTIVE-2-other'],
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
const getActiveBranchNames = () => {
|
|
177
|
+
return new Map([[WORKSPACE_PATH_A, new Set([activeBranch])]]);
|
|
178
|
+
};
|
|
179
|
+
await startServer(exec, getActiveBranchNames);
|
|
180
|
+
const data = await getLinks();
|
|
181
|
+
const activeLinks = data['ACTIVE-1'];
|
|
182
|
+
assert.ok(activeLinks, 'Should have ACTIVE-1 ticket');
|
|
183
|
+
assert.equal(activeLinks.length, 1);
|
|
184
|
+
assert.equal(activeLinks[0].hasActiveSession, true, 'Active branch should have hasActiveSession true');
|
|
185
|
+
const inactiveLinks = data['INACTIVE-2'];
|
|
186
|
+
assert.ok(inactiveLinks, 'Should have INACTIVE-2 ticket');
|
|
187
|
+
assert.equal(inactiveLinks[0].hasActiveSession, false, 'Inactive branch should have hasActiveSession false');
|
|
188
|
+
});
|
|
189
|
+
test('invalidateBranchLinkerCache forces fresh scan', async () => {
|
|
190
|
+
await stopServer();
|
|
191
|
+
invalidateBranchLinkerCache();
|
|
192
|
+
saveConfig(configPath, {
|
|
193
|
+
...DEFAULTS,
|
|
194
|
+
repos: [WORKSPACE_PATH_A],
|
|
195
|
+
});
|
|
196
|
+
let gitCallCount = 0;
|
|
197
|
+
const baseExec = makeMockExec({
|
|
198
|
+
branchesByPath: {
|
|
199
|
+
[WORKSPACE_PATH_A]: ['feature/SCAN-1-fresh'],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const countingExec = async (...args) => {
|
|
203
|
+
const [cmd] = args;
|
|
204
|
+
if (cmd === 'git')
|
|
205
|
+
gitCallCount++;
|
|
206
|
+
return baseExec(...args);
|
|
207
|
+
};
|
|
208
|
+
await startServer(countingExec);
|
|
209
|
+
// First request — populates module-level cache
|
|
210
|
+
const first = await getLinks();
|
|
211
|
+
assert.ok('SCAN-1' in first, 'Should have SCAN-1 after first request');
|
|
212
|
+
assert.equal(gitCallCount, 1, 'git should be called once on first request');
|
|
213
|
+
// Second request — served from cache
|
|
214
|
+
const second = await getLinks();
|
|
215
|
+
assert.ok('SCAN-1' in second);
|
|
216
|
+
assert.equal(gitCallCount, 1, 'git should not be called again within TTL');
|
|
217
|
+
// Invalidate cache
|
|
218
|
+
invalidateBranchLinkerCache();
|
|
219
|
+
// Third request — cache is cleared, should fetch fresh
|
|
220
|
+
const third = await getLinks();
|
|
221
|
+
assert.ok('SCAN-1' in third);
|
|
222
|
+
assert.equal(gitCallCount, 2, 'git should be called again after cache invalidation');
|
|
223
|
+
});
|
|
224
|
+
test('returns empty object when no workspaces', async () => {
|
|
225
|
+
await stopServer();
|
|
226
|
+
invalidateBranchLinkerCache();
|
|
227
|
+
saveConfig(configPath, {
|
|
228
|
+
...DEFAULTS,
|
|
229
|
+
repos: [],
|
|
230
|
+
});
|
|
231
|
+
// execAsync should never be called here
|
|
232
|
+
const exec = makeMockExec({});
|
|
233
|
+
await startServer(exec);
|
|
234
|
+
const data = await getLinks();
|
|
235
|
+
assert.equal(Object.keys(data).length, 0, 'Should return empty object when no workspaces configured');
|
|
236
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { MOUNTAIN_NAMES } from '../server/types.js';
|
|
4
|
+
import { branchToDisplayName } from '../server/git.js';
|
|
5
|
+
describe('MOUNTAIN_NAMES', () => {
|
|
6
|
+
test('contains 30 mountain names', () => {
|
|
7
|
+
assert.equal(MOUNTAIN_NAMES.length, 30);
|
|
8
|
+
});
|
|
9
|
+
test('all names are lowercase kebab-case', () => {
|
|
10
|
+
for (const name of MOUNTAIN_NAMES) {
|
|
11
|
+
assert.match(name, /^[a-z][a-z0-9-]*$/, `Mountain name "${name}" is not kebab-case`);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
test('no duplicate names', () => {
|
|
15
|
+
const unique = new Set(MOUNTAIN_NAMES);
|
|
16
|
+
assert.equal(unique.size, MOUNTAIN_NAMES.length);
|
|
17
|
+
});
|
|
18
|
+
test('cycling wraps around at array length', () => {
|
|
19
|
+
let idx = 28;
|
|
20
|
+
const name1 = MOUNTAIN_NAMES[idx % MOUNTAIN_NAMES.length];
|
|
21
|
+
idx++;
|
|
22
|
+
const name2 = MOUNTAIN_NAMES[idx % MOUNTAIN_NAMES.length];
|
|
23
|
+
idx++;
|
|
24
|
+
const name3 = MOUNTAIN_NAMES[idx % MOUNTAIN_NAMES.length];
|
|
25
|
+
assert.equal(name1, 'whitney');
|
|
26
|
+
assert.equal(name2, 'hood');
|
|
27
|
+
assert.equal(name3, 'everest'); // wraps back to start
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('branchToDisplayName', () => {
|
|
31
|
+
test('converts kebab-case to sentence case', () => {
|
|
32
|
+
assert.equal(branchToDisplayName('fix-mobile-scroll-bug'), 'Fix mobile scroll bug');
|
|
33
|
+
});
|
|
34
|
+
test('strips common branch prefixes', () => {
|
|
35
|
+
assert.equal(branchToDisplayName('feature/add-auth'), 'Add auth');
|
|
36
|
+
assert.equal(branchToDisplayName('fix/api-timeout'), 'Api timeout');
|
|
37
|
+
assert.equal(branchToDisplayName('chore/update-deps'), 'Update deps');
|
|
38
|
+
});
|
|
39
|
+
test('handles simple names', () => {
|
|
40
|
+
assert.equal(branchToDisplayName('lhotse'), 'Lhotse');
|
|
41
|
+
});
|
|
42
|
+
test('handles underscores', () => {
|
|
43
|
+
assert.equal(branchToDisplayName('fix_the_thing'), 'Fix the thing');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, afterEach } 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 { execFileSync } from 'node:child_process';
|
|
7
|
+
import { BranchWatcher } from '../server/watcher.js';
|
|
8
|
+
function makeTempGitRepo() {
|
|
9
|
+
// Resolve symlinks (macOS /var → /private/var) so paths match git output
|
|
10
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'branch-watcher-test-')));
|
|
11
|
+
execFileSync('git', ['init', '-b', 'main'], { cwd: dir });
|
|
12
|
+
execFileSync('git', [
|
|
13
|
+
'-c',
|
|
14
|
+
'user.name=Test',
|
|
15
|
+
'-c',
|
|
16
|
+
'user.email=test@test.com',
|
|
17
|
+
'commit',
|
|
18
|
+
'--allow-empty',
|
|
19
|
+
'-m',
|
|
20
|
+
'init',
|
|
21
|
+
], { cwd: dir });
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
describe('BranchWatcher', () => {
|
|
25
|
+
const cleanups = [];
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
for (const fn of cleanups) {
|
|
28
|
+
try {
|
|
29
|
+
fn();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* ignore */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
cleanups.length = 0;
|
|
36
|
+
});
|
|
37
|
+
it('detects branch change via HEAD file write', async () => {
|
|
38
|
+
const repoDir = makeTempGitRepo();
|
|
39
|
+
const parentDir = path.dirname(repoDir);
|
|
40
|
+
cleanups.push(() => fs.rmSync(repoDir, { recursive: true, force: true }));
|
|
41
|
+
const events = [];
|
|
42
|
+
const watcher = new BranchWatcher((cwdPath, newBranch) => {
|
|
43
|
+
events.push({ cwdPath, newBranch });
|
|
44
|
+
});
|
|
45
|
+
cleanups.push(() => watcher.close());
|
|
46
|
+
watcher.rebuild([parentDir]);
|
|
47
|
+
// Let fs.watch initialize
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
49
|
+
// Create the branch first, then simulate checkout by writing HEAD directly
|
|
50
|
+
// (more deterministic than git checkout which uses lock+rename)
|
|
51
|
+
execFileSync('git', ['branch', 'feature-test'], { cwd: repoDir });
|
|
52
|
+
const headPath = path.join(repoDir, '.git', 'HEAD');
|
|
53
|
+
fs.writeFileSync(headPath, 'ref: refs/heads/feature-test\n');
|
|
54
|
+
// Wait for debounce (300ms) + processing
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
56
|
+
assert.ok(events.length > 0, 'Expected at least one branch change event');
|
|
57
|
+
const lastEvent = events[events.length - 1];
|
|
58
|
+
assert.equal(lastEvent.cwdPath, repoDir);
|
|
59
|
+
assert.equal(lastEvent.newBranch, 'feature-test');
|
|
60
|
+
});
|
|
61
|
+
it('does not fire callback if branch did not change', async () => {
|
|
62
|
+
const repoDir = makeTempGitRepo();
|
|
63
|
+
const parentDir = path.dirname(repoDir);
|
|
64
|
+
cleanups.push(() => fs.rmSync(repoDir, { recursive: true, force: true }));
|
|
65
|
+
const events = [];
|
|
66
|
+
const watcher = new BranchWatcher((cwdPath, newBranch) => {
|
|
67
|
+
events.push({ cwdPath, newBranch });
|
|
68
|
+
});
|
|
69
|
+
cleanups.push(() => watcher.close());
|
|
70
|
+
watcher.rebuild([parentDir]);
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
72
|
+
// Touch the HEAD file without changing the branch content
|
|
73
|
+
const headPath = path.join(repoDir, '.git', 'HEAD');
|
|
74
|
+
const content = fs.readFileSync(headPath, 'utf-8');
|
|
75
|
+
fs.writeFileSync(headPath, content);
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
77
|
+
assert.equal(events.length, 0, 'Should not fire callback when branch is unchanged');
|
|
78
|
+
});
|
|
79
|
+
it('detects second branch change after atomic rename (inode change)', async () => {
|
|
80
|
+
const repoDir = makeTempGitRepo();
|
|
81
|
+
const parentDir = path.dirname(repoDir);
|
|
82
|
+
cleanups.push(() => fs.rmSync(repoDir, { recursive: true, force: true }));
|
|
83
|
+
const events = [];
|
|
84
|
+
const watcher = new BranchWatcher((cwdPath, newBranch) => {
|
|
85
|
+
events.push({ cwdPath, newBranch });
|
|
86
|
+
});
|
|
87
|
+
cleanups.push(() => watcher.close());
|
|
88
|
+
watcher.rebuild([parentDir]);
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
90
|
+
const headPath = path.join(repoDir, '.git', 'HEAD');
|
|
91
|
+
// First change: simulate git's atomic checkout (write to .lock, rename over HEAD)
|
|
92
|
+
// This changes the inode, which would kill kqueue-based watchers
|
|
93
|
+
execFileSync('git', ['branch', 'branch-one'], { cwd: repoDir });
|
|
94
|
+
const lockPath = headPath + '.lock';
|
|
95
|
+
fs.writeFileSync(lockPath, 'ref: refs/heads/branch-one\n');
|
|
96
|
+
fs.renameSync(lockPath, headPath);
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
98
|
+
assert.ok(events.length >= 1, 'Expected at least one event after first atomic rename');
|
|
99
|
+
assert.equal(events[events.length - 1].newBranch, 'branch-one');
|
|
100
|
+
// Second change: simulate another atomic checkout — this would fail without
|
|
101
|
+
// watcher recreation because the old watcher tracked the deleted inode
|
|
102
|
+
execFileSync('git', ['branch', 'branch-two'], { cwd: repoDir });
|
|
103
|
+
const lockPath2 = headPath + '.lock';
|
|
104
|
+
fs.writeFileSync(lockPath2, 'ref: refs/heads/branch-two\n');
|
|
105
|
+
fs.renameSync(lockPath2, headPath);
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
107
|
+
const secondChange = events.find((e) => e.newBranch === 'branch-two');
|
|
108
|
+
assert.ok(secondChange, 'Expected branch-two event after second atomic rename (watcher must survive inode change)');
|
|
109
|
+
});
|
|
110
|
+
it('closes cleanly', () => {
|
|
111
|
+
const watcher = new BranchWatcher(() => { });
|
|
112
|
+
watcher.close();
|
|
113
|
+
// No error means success
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { test, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
let tmpDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browser-cli-test-'));
|
|
10
|
+
fs.writeFileSync(path.join(tmpDir, 'test.html'), '<h1>Test</h1>');
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
test('browser command with no args prints usage and exits 1', () => {
|
|
16
|
+
try {
|
|
17
|
+
execFileSync('node', ['dist/bin/claude-remote-cli.js', 'browser'], {
|
|
18
|
+
encoding: 'utf-8',
|
|
19
|
+
env: { ...process.env, PATH: process.env.PATH },
|
|
20
|
+
});
|
|
21
|
+
assert.fail('Should have exited with code 1');
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
const e = err;
|
|
25
|
+
assert.strictEqual(e.status, 1);
|
|
26
|
+
assert.ok((e.stderr ?? '').includes('Usage'));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
test('browser --help shows usage and exits 0', () => {
|
|
30
|
+
try {
|
|
31
|
+
const output = execFileSync('node', ['dist/bin/claude-remote-cli.js', 'browser', '--help'], {
|
|
32
|
+
encoding: 'utf-8',
|
|
33
|
+
env: { ...process.env, PATH: process.env.PATH },
|
|
34
|
+
});
|
|
35
|
+
assert.ok(output.includes('Usage') || output.includes('browser'));
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const e = err;
|
|
39
|
+
// --help may print to stderr
|
|
40
|
+
const out = (e.stdout ?? '') + (e.stderr ?? '');
|
|
41
|
+
assert.ok(out.includes('Usage') || out.includes('browser'));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
test('browser command fails gracefully when server is not running', () => {
|
|
45
|
+
try {
|
|
46
|
+
execFileSync('node', [
|
|
47
|
+
'dist/bin/claude-remote-cli.js',
|
|
48
|
+
'browser',
|
|
49
|
+
path.join(tmpDir, 'test.html'),
|
|
50
|
+
], {
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
env: {
|
|
53
|
+
...process.env,
|
|
54
|
+
CLAUDE_REMOTE_PORT: '19999',
|
|
55
|
+
CLAUDE_REMOTE_BROWSER_TOKEN: 'test-token',
|
|
56
|
+
PATH: process.env.PATH,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
assert.fail('Should have exited with error');
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const e = err;
|
|
63
|
+
assert.ok(e.status !== 0);
|
|
64
|
+
assert.ok((e.stderr ?? '').includes('connect') ||
|
|
65
|
+
(e.stderr ?? '').includes('ECONNREFUSED') ||
|
|
66
|
+
(e.stderr ?? '').includes('Error'));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
test('browser command fails when token not set', () => {
|
|
70
|
+
try {
|
|
71
|
+
execFileSync('node', [
|
|
72
|
+
'dist/bin/claude-remote-cli.js',
|
|
73
|
+
'browser',
|
|
74
|
+
path.join(tmpDir, 'test.html'),
|
|
75
|
+
], {
|
|
76
|
+
encoding: 'utf-8',
|
|
77
|
+
env: {
|
|
78
|
+
...process.env,
|
|
79
|
+
CLAUDE_REMOTE_PORT: '19999',
|
|
80
|
+
CLAUDE_REMOTE_BROWSER_TOKEN: '', // empty token
|
|
81
|
+
PATH: process.env.PATH,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
assert.fail('Should have exited with error');
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const e = err;
|
|
88
|
+
assert.ok(e.status !== 0);
|
|
89
|
+
assert.ok((e.stderr ?? '').includes('CLAUDE_REMOTE_BROWSER_TOKEN'));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { test, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { createBrowserToken, validateToken, resolveTokenPath, generateScopedToken, validateScopedToken, cleanExpiredTokens, getTokenForPath, _resetForTesting, } from '../server/browser-content.js';
|
|
7
|
+
let tmpDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
_resetForTesting();
|
|
10
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browser-content-test-'));
|
|
11
|
+
fs.writeFileSync(path.join(tmpDir, 'index.html'), '<h1>Hello</h1>');
|
|
12
|
+
fs.writeFileSync(path.join(tmpDir, 'styles.css'), 'body { color: red; }');
|
|
13
|
+
fs.mkdirSync(path.join(tmpDir, 'sub'));
|
|
14
|
+
fs.writeFileSync(path.join(tmpDir, 'sub', 'nested.js'), 'console.log("hi")');
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
// Content token tests
|
|
20
|
+
test('createBrowserToken returns a token string', () => {
|
|
21
|
+
const token = createBrowserToken(path.join(tmpDir, 'index.html'));
|
|
22
|
+
assert.ok(typeof token === 'string');
|
|
23
|
+
assert.ok(token.length > 0);
|
|
24
|
+
});
|
|
25
|
+
test('validateToken returns baseDir for valid token', () => {
|
|
26
|
+
const filePath = path.join(tmpDir, 'index.html');
|
|
27
|
+
const token = createBrowserToken(filePath);
|
|
28
|
+
const result = validateToken(token);
|
|
29
|
+
assert.ok(result);
|
|
30
|
+
assert.strictEqual(result.baseDir, tmpDir);
|
|
31
|
+
});
|
|
32
|
+
test('validateToken returns null for invalid token', () => {
|
|
33
|
+
assert.strictEqual(validateToken('nonexistent-token'), null);
|
|
34
|
+
});
|
|
35
|
+
test('resolveTokenPath serves file within baseDir', () => {
|
|
36
|
+
const filePath = path.join(tmpDir, 'index.html');
|
|
37
|
+
const token = createBrowserToken(filePath);
|
|
38
|
+
assert.strictEqual(resolveTokenPath(token, 'index.html'), filePath);
|
|
39
|
+
});
|
|
40
|
+
test('resolveTokenPath serves nested file', () => {
|
|
41
|
+
const filePath = path.join(tmpDir, 'index.html');
|
|
42
|
+
const token = createBrowserToken(filePath);
|
|
43
|
+
assert.strictEqual(resolveTokenPath(token, 'sub/nested.js'), path.join(tmpDir, 'sub', 'nested.js'));
|
|
44
|
+
});
|
|
45
|
+
test('resolveTokenPath rejects path traversal', () => {
|
|
46
|
+
const token = createBrowserToken(path.join(tmpDir, 'index.html'));
|
|
47
|
+
assert.strictEqual(resolveTokenPath(token, '../../../etc/passwd'), null);
|
|
48
|
+
});
|
|
49
|
+
test('resolveTokenPath rejects absolute path in relative', () => {
|
|
50
|
+
const token = createBrowserToken(path.join(tmpDir, 'index.html'));
|
|
51
|
+
assert.strictEqual(resolveTokenPath(token, '/etc/passwd'), null);
|
|
52
|
+
});
|
|
53
|
+
// Scoped auth token tests
|
|
54
|
+
test('generateScopedToken returns a hex string', () => {
|
|
55
|
+
const token = generateScopedToken();
|
|
56
|
+
assert.ok(/^[a-f0-9]+$/.test(token));
|
|
57
|
+
});
|
|
58
|
+
test('validateScopedToken returns true for correct token', () => {
|
|
59
|
+
const token = generateScopedToken();
|
|
60
|
+
assert.strictEqual(validateScopedToken(token), true);
|
|
61
|
+
});
|
|
62
|
+
test('validateScopedToken returns false for wrong token', () => {
|
|
63
|
+
generateScopedToken();
|
|
64
|
+
assert.strictEqual(validateScopedToken('wrong-token'), false);
|
|
65
|
+
});
|
|
66
|
+
// Token expiry tests
|
|
67
|
+
test('cleanExpiredTokens removes old tokens', () => {
|
|
68
|
+
const token = createBrowserToken(path.join(tmpDir, 'index.html'));
|
|
69
|
+
assert.ok(validateToken(token));
|
|
70
|
+
cleanExpiredTokens(0); // TTL of 0ms = everything expired
|
|
71
|
+
assert.strictEqual(validateToken(token), null);
|
|
72
|
+
});
|
|
73
|
+
test('cleanExpiredTokens keeps fresh tokens', () => {
|
|
74
|
+
const token = createBrowserToken(path.join(tmpDir, 'index.html'));
|
|
75
|
+
cleanExpiredTokens(24 * 60 * 60 * 1000);
|
|
76
|
+
assert.ok(validateToken(token));
|
|
77
|
+
});
|
|
78
|
+
// Idempotent token creation
|
|
79
|
+
test('createBrowserToken for same path returns existing token', () => {
|
|
80
|
+
const filePath = path.join(tmpDir, 'index.html');
|
|
81
|
+
const token1 = createBrowserToken(filePath);
|
|
82
|
+
const token2 = createBrowserToken(filePath);
|
|
83
|
+
assert.strictEqual(token1, token2);
|
|
84
|
+
});
|
|
85
|
+
// getTokenForPath
|
|
86
|
+
test('getTokenForPath returns token for known path', () => {
|
|
87
|
+
const filePath = path.join(tmpDir, 'index.html');
|
|
88
|
+
const token = createBrowserToken(filePath);
|
|
89
|
+
assert.strictEqual(getTokenForPath(filePath), token);
|
|
90
|
+
});
|
|
91
|
+
test('getTokenForPath returns null for unknown path', () => {
|
|
92
|
+
assert.strictEqual(getTokenForPath('/no/such/file'), null);
|
|
93
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
// Test the pure logic of tab identity and deduplication.
|
|
4
|
+
// We can't import .svelte.ts files in node:test (they need the Svelte compiler),
|
|
5
|
+
// so we test the logic by duplicating the key algorithm here.
|
|
6
|
+
function tabKey(filePath, tabType) {
|
|
7
|
+
return `${tabType}::${filePath}`;
|
|
8
|
+
}
|
|
9
|
+
test('tabKey differentiates same file with different types', () => {
|
|
10
|
+
const diffKey = tabKey('/tmp/index.html', 'diff');
|
|
11
|
+
const htmlKey = tabKey('/tmp/index.html', 'html');
|
|
12
|
+
assert.notStrictEqual(diffKey, htmlKey);
|
|
13
|
+
});
|
|
14
|
+
test('tabKey matches same file with same type', () => {
|
|
15
|
+
const key1 = tabKey('/tmp/index.html', 'html');
|
|
16
|
+
const key2 = tabKey('/tmp/index.html', 'html');
|
|
17
|
+
assert.strictEqual(key1, key2);
|
|
18
|
+
});
|
|
19
|
+
test('openHtmlTab logic creates correct tab shape', () => {
|
|
20
|
+
const filePath = '/tmp/gstack-sketch/design-board.html';
|
|
21
|
+
const tab = {
|
|
22
|
+
filePath,
|
|
23
|
+
fileName: filePath.split('/').pop() ?? filePath,
|
|
24
|
+
isChanged: false,
|
|
25
|
+
tabType: 'html',
|
|
26
|
+
token: 'abc123',
|
|
27
|
+
};
|
|
28
|
+
assert.strictEqual(tab.fileName, 'design-board.html');
|
|
29
|
+
assert.strictEqual(tab.isChanged, false);
|
|
30
|
+
assert.strictEqual(tab.tabType, 'html');
|
|
31
|
+
assert.strictEqual(tab.token, 'abc123');
|
|
32
|
+
});
|
|
33
|
+
test('refreshHtmlTab logic uses version counter', () => {
|
|
34
|
+
const baseUrl = '/browser-content/abc123/design-board.html';
|
|
35
|
+
const refreshVersion = 1;
|
|
36
|
+
const refreshed = `${baseUrl}?v=${refreshVersion}`;
|
|
37
|
+
assert.ok(refreshed.includes('?v='));
|
|
38
|
+
assert.ok(refreshed.startsWith(baseUrl));
|
|
39
|
+
});
|