@cc-monitoring/proxy-core 0.2.3 → 0.3.1
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/dist/docs/404.html +1 -1
- package/dist/docs/assets/{app.6_CicOIJ.js → app.B9A39uFv.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.B6Qfxe6W.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.ZMWZsg8z.js → VPLocalSearchBox.DdhC696L.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BkvhsjzH.js → theme.CfB2tOFi.js} +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/overview.html +2 -2
- package/dist/docs/packages-utils.html +2 -2
- package/dist/docs/proxy-admin/env-and-build.html +2 -2
- package/dist/docs/proxy-admin/index.html +2 -2
- package/dist/docs/proxy-admin/layout-and-menu.html +2 -2
- package/dist/docs/proxy-admin/pages.html +2 -2
- package/dist/docs/proxy-admin/routing-and-auth.html +2 -2
- package/dist/docs/proxy-admin/services-and-mock.html +2 -2
- package/dist/docs/proxy-cli/api-layer.html +2 -2
- package/dist/docs/proxy-cli/app-shell.html +2 -2
- package/dist/docs/proxy-cli/components-common.html +2 -2
- package/dist/docs/proxy-cli/components-config.html +2 -2
- package/dist/docs/proxy-cli/components-requests.html +2 -2
- package/dist/docs/proxy-cli/components-stats.html +2 -2
- package/dist/docs/proxy-cli/components-status.html +2 -2
- package/dist/docs/proxy-cli/data-hooks.html +2 -2
- package/dist/docs/proxy-cli/index.html +2 -2
- package/dist/docs/proxy-cli/utils-and-i18n.html +2 -2
- package/dist/docs/proxy-core/chat-api.html +2 -2
- package/dist/docs/proxy-core/cli.html +2 -2
- package/dist/docs/proxy-core/config.html +2 -2
- package/dist/docs/proxy-core/data-layer.html +2 -2
- package/dist/docs/proxy-core/entry.html +2 -2
- package/dist/docs/proxy-core/index.html +2 -2
- package/dist/docs/proxy-core/pm2.html +2 -2
- package/dist/docs/proxy-core/providers.html +2 -2
- package/dist/docs/proxy-core/proxy.html +2 -2
- package/dist/docs/proxy-core/stats.html +2 -2
- package/dist/docs/proxy-core/tool.html +2 -2
- package/dist/docs/proxy-core/ui-routes.html +2 -2
- package/dist/docs/proxy-core/utilities.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/request-flow.html +2 -2
- package/dist/mcp-server/index.js +5 -3
- package/dist/mcp-server/tools.js +24 -2
- package/dist/server/__tests__/server.test.js +9 -1
- package/dist/server/__tests__/ui-route.test.js +8 -0
- package/dist/server/chat-route.js +6 -0
- package/dist/server/proxy.js +5 -0
- package/dist/server/ui-route.js +113 -7
- package/dist/ui/assets/{1c-HTPXddJd.js → 1c-DAu68mP4.js} +1 -1
- package/dist/ui/assets/{abnf-bINSkl_m.js → abnf-Cu8RHCHK.js} +1 -1
- package/dist/ui/assets/{abnfDiagram-VRR7QNED-ClDzCbvw.js → abnfDiagram-VRR7QNED-BUBToPXF.js} +1 -1
- package/dist/ui/assets/{accesslog-BAnIWzpe.js → accesslog-BG-34XOy.js} +1 -1
- package/dist/ui/assets/{actionscript-DVcm6F_C.js → actionscript-iw0HHMGM.js} +1 -1
- package/dist/ui/assets/{ada-Cxn-QX4j.js → ada-qRqj6VGQ.js} +1 -1
- package/dist/ui/assets/{angelscript-BAwAPvBQ.js → angelscript-BeaXyx_P.js} +1 -1
- package/dist/ui/assets/{apache-CjjovgUx.js → apache-CU8d-z9X.js} +1 -1
- package/dist/ui/assets/{applescript-VUcyXoVh.js → applescript-d1Wxkqz1.js} +1 -1
- package/dist/ui/assets/{arc-3Nou_aku.js → arc-BMsPIm3j.js} +1 -1
- package/dist/ui/assets/{arcade-Dz2VAk7O.js → arcade-CtbgrWTM.js} +1 -1
- package/dist/ui/assets/{architectureDiagram-ZJ3FMSHR-DhwSsCdN.js → architectureDiagram-ZJ3FMSHR-CVjC5TrS.js} +1 -1
- package/dist/ui/assets/{arduino-B-Ptb1K1.js → arduino-Bc9L39Hr.js} +1 -1
- package/dist/ui/assets/{armasm-DU_6qZlW.js → armasm-DuWmpjWR.js} +1 -1
- package/dist/ui/assets/{asciidoc-DYbYiWKT.js → asciidoc-CDIhFTnx.js} +1 -1
- package/dist/ui/assets/{aspectj-CmdNkqAs.js → aspectj-SYj-57Es.js} +1 -1
- package/dist/ui/assets/{autohotkey-if9QUvYo.js → autohotkey-hBi5dq37.js} +1 -1
- package/dist/ui/assets/{autoit-DGU3G1s6.js → autoit-BSawXzhg.js} +1 -1
- package/dist/ui/assets/{avrasm-1zsEtPJ6.js → avrasm-CwYom8Np.js} +1 -1
- package/dist/ui/assets/{awk-CvmeshSq.js → awk-BvBRfGo2.js} +1 -1
- package/dist/ui/assets/{axapta-CbJGb5cN.js → axapta-CqUavA8k.js} +1 -1
- package/dist/ui/assets/{bash-CFXg_i2e.js → bash-BS_ZnmEu.js} +1 -1
- package/dist/ui/assets/{basic-6atic5cT.js → basic-xbP4IXrN.js} +1 -1
- package/dist/ui/assets/{blockDiagram-677ZJIJ3-BKXinDkB.js → blockDiagram-677ZJIJ3-CmpI3f5p.js} +1 -1
- package/dist/ui/assets/{bnf-DydAtPHV.js → bnf-BECwu6uW.js} +1 -1
- package/dist/ui/assets/{brainfuck-CnHx1vO0.js → brainfuck-QQgu4j4N.js} +1 -1
- package/dist/ui/assets/{coq-BShzwzh7.js → c-BRJEPYra.js} +1 -1
- package/dist/ui/assets/{c-like-XpQV9S6j.js → c-like-DYrFOtXy.js} +1 -1
- package/dist/ui/assets/{c4Diagram-LMCZKHZV-D5EmWl5d.js → c4Diagram-LMCZKHZV-B6Hgngz9.js} +1 -1
- package/dist/ui/assets/{cal-D-orUuBW.js → cal-CNMrJ6v5.js} +1 -1
- package/dist/ui/assets/{capnproto-TdI6NT8O.js → capnproto-DYCKl6oY.js} +1 -1
- package/dist/ui/assets/{ceylon-Dji8eqtG.js → ceylon-mb8tRPl0.js} +1 -1
- package/dist/ui/assets/channel-Bc4Td8eZ.js +1 -0
- package/dist/ui/assets/{chunk-2Q5K7J3B-CaIvwVWl.js → chunk-2Q5K7J3B-LxvQ-G67.js} +1 -1
- package/dist/ui/assets/{chunk-32BRIVSS-C7oMpoeu.js → chunk-32BRIVSS-DxzKK6Aw.js} +1 -1
- package/dist/ui/assets/{chunk-5VM5RSS4-BQ0aVGl-.js → chunk-5VM5RSS4-CZEoYJ2W.js} +1 -1
- package/dist/ui/assets/{chunk-EX3LRPZG-D-_EqojZ.js → chunk-EX3LRPZG-DS2PytrN.js} +1 -1
- package/dist/ui/assets/{chunk-JWPE2WC7-B0OLx2rk.js → chunk-JWPE2WC7-UX0Sr1Ie.js} +1 -1
- package/dist/ui/assets/{chunk-MOJQB5TN-Be8IXTUE.js → chunk-MOJQB5TN-YbJgT9t4.js} +1 -1
- package/dist/ui/assets/{chunk-RYQCIY6F-DceCW2mS.js → chunk-RYQCIY6F-DS2VH_U9.js} +1 -1
- package/dist/ui/assets/{chunk-V7JOEXUC-Cbhq5qgO.js → chunk-V7JOEXUC-CZr0pkI1.js} +1 -1
- package/dist/ui/assets/{chunk-VR4S4FIN-D6WJuaS1.js → chunk-VR4S4FIN-50opPAD3.js} +1 -1
- package/dist/ui/assets/{chunk-XXDRQBXY-CKrih4pC.js → chunk-XXDRQBXY-DAAX9l7U.js} +1 -1
- package/dist/ui/assets/classDiagram-OUVF2IWQ-BicxhZFq.js +1 -0
- package/dist/ui/assets/classDiagram-v2-EOCWNBFH-BicxhZFq.js +1 -0
- package/dist/ui/assets/{clean-0frlVN_A.js → clean-C4X7dSkh.js} +1 -1
- package/dist/ui/assets/{clojure-D0q_yQDf.js → clojure-DSg9gUa7.js} +1 -1
- package/dist/ui/assets/{clojure-repl-DgYhEzOe.js → clojure-repl-CagRYx7h.js} +1 -1
- package/dist/ui/assets/{cmake-WF5tF5xU.js → cmake-CFdBn53c.js} +1 -1
- package/dist/ui/assets/{coffeescript-CyWYgeyv.js → coffeescript-CJZtMOPx.js} +1 -1
- package/dist/ui/assets/{c-DarYnPBd.js → coq-IDMIFfem.js} +1 -1
- package/dist/ui/assets/{cos-C5i3_TIB.js → cos-Bx2VJuzG.js} +1 -1
- package/dist/ui/assets/{cose-bilkent-JH36ORCC-BCwilS6I.js → cose-bilkent-JH36ORCC-zWRLsZpZ.js} +1 -1
- package/dist/ui/assets/{cpp-F1mRQFyw.js → cpp-CsG6DlC4.js} +1 -1
- package/dist/ui/assets/{crmsh-BAkHUu-B.js → crmsh-DTy9MnmJ.js} +1 -1
- package/dist/ui/assets/{crystal-CwUaomfj.js → crystal-BwgrLrZ1.js} +1 -1
- package/dist/ui/assets/{csharp-B1gHIdjK.js → csharp-B2VeTPAF.js} +1 -1
- package/dist/ui/assets/{csp-CblCTMvV.js → csp-CTpydNhH.js} +1 -1
- package/dist/ui/assets/{css-CXg7kJV3.js → css-B0mMUbpY.js} +1 -1
- package/dist/ui/assets/{cynefin-VYW2F7L2-BdgMlR5e.js → cynefin-VYW2F7L2-BmjQEj6q.js} +1 -1
- package/dist/ui/assets/{cynefinDiagram-TSTJHNR4-Bozdux8p.js → cynefinDiagram-TSTJHNR4-Dr0Zrke0.js} +1 -1
- package/dist/ui/assets/{d-Cs5yNCaM.js → d-DjuDZnbI.js} +1 -1
- package/dist/ui/assets/{dagre-VKFMJZFB-tI_mjNMn.js → dagre-VKFMJZFB-xoTPT2Uu.js} +1 -1
- package/dist/ui/assets/{dart-BqgylVkh.js → dart-xR3GU9TJ.js} +1 -1
- package/dist/ui/assets/{delphi-CbnfHpIi.js → delphi-bJas5QnG.js} +1 -1
- package/dist/ui/assets/{diagram-FQU43EPY-DmyL3OJT.js → diagram-FQU43EPY-rL_IU7gU.js} +1 -1
- package/dist/ui/assets/{diagram-G47NLZAW-BEi9GZlZ.js → diagram-G47NLZAW-EcDiCVGj.js} +1 -1
- package/dist/ui/assets/{diagram-NH7WQ7WH-Bct44Oh1.js → diagram-NH7WQ7WH-CGGXOjev.js} +1 -1
- package/dist/ui/assets/{diagram-OA4YK3LP-VWAgZxPK.js → diagram-OA4YK3LP-DnE7hDjD.js} +1 -1
- package/dist/ui/assets/{diagram-WEI45ONY-CxcHsE6q.js → diagram-WEI45ONY-CTJH5eD8.js} +1 -1
- package/dist/ui/assets/{diff-CMJ2hGQW.js → diff-doJFF3fv.js} +1 -1
- package/dist/ui/assets/{django-Cq_XIhVZ.js → django-D3b-owa_.js} +1 -1
- package/dist/ui/assets/{dns-CJs3z6-E.js → dns-C42KYV-Z.js} +1 -1
- package/dist/ui/assets/{dockerfile-ehs0ogry.js → dockerfile-9KUINc2j.js} +1 -1
- package/dist/ui/assets/{dos-DXOYESf1.js → dos-CxnlJ6qp.js} +1 -1
- package/dist/ui/assets/{dsconfig-Cs2hs3Bl.js → dsconfig-6CMMCwmY.js} +1 -1
- package/dist/ui/assets/{dts-Dk8yoweb.js → dts-D78rdUIB.js} +1 -1
- package/dist/ui/assets/{dust-BMK9e-Ss.js → dust-Dv1uTfJU.js} +1 -1
- package/dist/ui/assets/{ebnf-DlyBI5m6.js → ebnf-BGbG1cFK.js} +1 -1
- package/dist/ui/assets/{ebnfDiagram-CCIWWBDH-DBp9JHEq.js → ebnfDiagram-CCIWWBDH-z2VgRSUV.js} +1 -1
- package/dist/ui/assets/{elixir-BThlsblT.js → elixir-ZgAQVY1J.js} +1 -1
- package/dist/ui/assets/{elm-DrmZwmcc.js → elm-CudiFuNu.js} +1 -1
- package/dist/ui/assets/{erDiagram-Q63AITRT-BhK4NKvx.js → erDiagram-Q63AITRT-X1TODuRv.js} +1 -1
- package/dist/ui/assets/{erb-BQM7j3K9.js → erb-Bz_NZh6W.js} +1 -1
- package/dist/ui/assets/{erlang-CuUaWEXv.js → erlang-DNuj1nb2.js} +1 -1
- package/dist/ui/assets/{erlang-repl-DB0uphao.js → erlang-repl-SP1Gan9W.js} +1 -1
- package/dist/ui/assets/{excel-DmsuWUss.js → excel-0K77uECF.js} +1 -1
- package/dist/ui/assets/{fix-D-Uzxpz_.js → fix-CalJq7JD.js} +1 -1
- package/dist/ui/assets/{flix-B_7EHWiA.js → flix-BlIs11_w.js} +1 -1
- package/dist/ui/assets/{flowDiagram-23GEKE2U-DtMG1G-h.js → flowDiagram-23GEKE2U-DA2dnmc0.js} +1 -1
- package/dist/ui/assets/{fortran-Djp5tH9W.js → fortran-ZCobXY2R.js} +1 -1
- package/dist/ui/assets/{fsharp-Pj_2ZEa4.js → fsharp-Be8rwUtc.js} +1 -1
- package/dist/ui/assets/{gams-B2i3v2ec.js → gams-DUat8frQ.js} +1 -1
- package/dist/ui/assets/{ganttDiagram-NO4QXBWP-C7M-JA0W.js → ganttDiagram-NO4QXBWP-DdFwaIGZ.js} +1 -1
- package/dist/ui/assets/{gauss-DaV8sHLU.js → gauss-BB4U88dy.js} +1 -1
- package/dist/ui/assets/{gcode-0Jvrh6HY.js → gcode-DzeUZuZg.js} +1 -1
- package/dist/ui/assets/{gherkin-DoOl28D6.js → gherkin-B_8vW_BO.js} +1 -1
- package/dist/ui/assets/{gitGraphDiagram-IHSO6WYX-D3HB5mow.js → gitGraphDiagram-IHSO6WYX-B-Dn8x6I.js} +1 -1
- package/dist/ui/assets/{glsl-DEbk7rH5.js → glsl-DGHGjPC1.js} +1 -1
- package/dist/ui/assets/{gml-CBcqXBNV.js → gml-BYEku8Yh.js} +1 -1
- package/dist/ui/assets/{go-DqesmqF8.js → go-D8aSgQMc.js} +1 -1
- package/dist/ui/assets/{golo-9U1oO0b5.js → golo-CDjCU_jh.js} +1 -1
- package/dist/ui/assets/{gradle-DZliBXTY.js → gradle-BeUw7tl_.js} +1 -1
- package/dist/ui/assets/{groovy-Dys3GQwv.js → groovy-5iCQOtR3.js} +1 -1
- package/dist/ui/assets/{haml-CrtAODl1.js → haml-BEdL8FJi.js} +1 -1
- package/dist/ui/assets/{handlebars-CueXuqSj.js → handlebars-BuYQNzoK.js} +1 -1
- package/dist/ui/assets/{haskell-CRungOQt.js → haskell-I4FCBcW0.js} +1 -1
- package/dist/ui/assets/{haxe-aMz7L4gD.js → haxe-yigyKfFr.js} +1 -1
- package/dist/ui/assets/{hsp-Cw75yqGb.js → hsp-ZUk4j7d1.js} +1 -1
- package/dist/ui/assets/{htmlbars-e_mpUZNH.js → htmlbars-QYP3lT3u.js} +1 -1
- package/dist/ui/assets/{http-Di7E_m1o.js → http-BJpD1O71.js} +1 -1
- package/dist/ui/assets/{hy-CwlvsNj6.js → hy-CIJ1bP6S.js} +1 -1
- package/dist/ui/assets/{index-BdSumlU0.js → index-BDXWttnT.js} +3 -3
- package/dist/ui/assets/index-C0fxlU9d.css +10 -0
- package/dist/ui/assets/{index-BVM6YOjH.js → index-COAgrx1J.js} +256 -256
- package/dist/ui/assets/{infoDiagram-FWYZ7A6U-_r-6eAzx.js → infoDiagram-FWYZ7A6U-fTg9eLNt.js} +1 -1
- package/dist/ui/assets/{inform7-dwg1imm1.js → inform7-DAsu-lC2.js} +1 -1
- package/dist/ui/assets/{ini-DEXL1rCA.js → ini-Bhz-zHCg.js} +1 -1
- package/dist/ui/assets/{irpf90-Ccs_8Sp-.js → irpf90-BunhbwBl.js} +1 -1
- package/dist/ui/assets/{isbl-B9d1nYmL.js → isbl-q9L6hOhP.js} +1 -1
- package/dist/ui/assets/{ishikawaDiagram-FXEZZL3T-lNCezpU4.js → ishikawaDiagram-FXEZZL3T-B8G0sdgd.js} +1 -1
- package/dist/ui/assets/{java-5fv2smM6.js → java-Iks5Pkau.js} +1 -1
- package/dist/ui/assets/{javascript-TAWm0NK8.js → javascript-C4vltXPw.js} +1 -1
- package/dist/ui/assets/{jboss-cli-dM1kcsbR.js → jboss-cli-BQBvd2X2.js} +1 -1
- package/dist/ui/assets/{journeyDiagram-5HDEW3XC-CBYHux8i.js → journeyDiagram-5HDEW3XC-Crbzgrum.js} +1 -1
- package/dist/ui/assets/{json-CtCzm6jF.js → json-DXK0_jUK.js} +1 -1
- package/dist/ui/assets/{julia-Co7H3kzk.js → julia-CCYoBiYK.js} +1 -1
- package/dist/ui/assets/{julia-repl-BT1ar5VS.js → julia-repl-y8v2Oorl.js} +1 -1
- package/dist/ui/assets/{kanban-definition-HUTT4EX6-B8x8eQyo.js → kanban-definition-HUTT4EX6-C8sCOsBX.js} +1 -1
- package/dist/ui/assets/{kotlin-W6mVKIj9.js → kotlin-Bnjk7_fw.js} +1 -1
- package/dist/ui/assets/{lasso-DJG4luGz.js → lasso-B-al6x63.js} +1 -1
- package/dist/ui/assets/{latex-DbBOxO7u.js → latex-BUmqlb9r.js} +1 -1
- package/dist/ui/assets/{ldif-CVS189JA.js → ldif-D42RWj6Q.js} +1 -1
- package/dist/ui/assets/{leaf-eYNiXTKP.js → leaf-CrH4BZ0m.js} +1 -1
- package/dist/ui/assets/{less-DxhvWM95.js → less-upmRlQQW.js} +1 -1
- package/dist/ui/assets/{lisp-SC7AwnXC.js → lisp-DHYS22SA.js} +1 -1
- package/dist/ui/assets/{livecodeserver-CR2eHihF.js → livecodeserver-CeQNmFP3.js} +1 -1
- package/dist/ui/assets/{livescript-DBZ-GaVK.js → livescript-PJWpY1JI.js} +1 -1
- package/dist/ui/assets/{llvm-BzURO4tt.js → llvm-BKtKWwCt.js} +1 -1
- package/dist/ui/assets/{lsl-DGoJxEUu.js → lsl-RWRLRjk7.js} +1 -1
- package/dist/ui/assets/{lua-qgG9gdiw.js → lua-BFUDiPGm.js} +1 -1
- package/dist/ui/assets/{makefile-D0kZ6SOe.js → makefile-CUDyOz5D.js} +1 -1
- package/dist/ui/assets/{markdown-DRQWgMxk.js → markdown-zZCLypc6.js} +1 -1
- package/dist/ui/assets/{mathematica-DIBvZyEZ.js → mathematica-BHbxKxnq.js} +1 -1
- package/dist/ui/assets/{matlab-BNg0BR0b.js → matlab-DlGtg--p.js} +1 -1
- package/dist/ui/assets/{maxima-Bpga3G1G.js → maxima-BBBeS3Ra.js} +1 -1
- package/dist/ui/assets/{mel-CMOJa9Mg.js → mel-nh8j7W_6.js} +1 -1
- package/dist/ui/assets/{mercury-uvrRzupw.js → mercury-DyyD-pqr.js} +1 -1
- package/dist/ui/assets/{mindmap-definition-LN4V7U3C-DK8j_8Ap.js → mindmap-definition-LN4V7U3C-CJlhDHqM.js} +1 -1
- package/dist/ui/assets/{mipsasm-p1gWpDuw.js → mipsasm-hiEhRRiq.js} +1 -1
- package/dist/ui/assets/{mizar-DV1ENOOU.js → mizar-BZa3iAAL.js} +1 -1
- package/dist/ui/assets/{mojolicious-tg5XQSro.js → mojolicious-BCIm2V_y.js} +1 -1
- package/dist/ui/assets/{monkey-D8mYa31Q.js → monkey-zmK-7N8b.js} +1 -1
- package/dist/ui/assets/{moonscript-1d_rtlyv.js → moonscript-BsS_Mn3D.js} +1 -1
- package/dist/ui/assets/{n1ql-C5srRxme.js → n1ql-D7xVgDP0.js} +1 -1
- package/dist/ui/assets/{nginx-xS1muaWm.js → nginx-ct2q4dtd.js} +1 -1
- package/dist/ui/assets/{nim-CGGiMQfp.js → nim-RO0UOmW3.js} +1 -1
- package/dist/ui/assets/{nix-C77HsR7m.js → nix-BcRJxPKH.js} +1 -1
- package/dist/ui/assets/{node-repl-CSeYusSM.js → node-repl-DgIefwxW.js} +1 -1
- package/dist/ui/assets/{nsis-BffC7mSW.js → nsis-DqHs9QhB.js} +1 -1
- package/dist/ui/assets/{objectivec-WpjnmlJB.js → objectivec-BBcWqc7Y.js} +1 -1
- package/dist/ui/assets/{ocaml-CHUkJkjB.js → ocaml-BExfw7D6.js} +1 -1
- package/dist/ui/assets/{openscad-BvImqFT0.js → openscad-CHPRELQb.js} +1 -1
- package/dist/ui/assets/{oxygene-BMBykbf5.js → oxygene-DpS9uMJW.js} +1 -1
- package/dist/ui/assets/{parser3-lRbuZW0M.js → parser3-CjJ_QdBp.js} +1 -1
- package/dist/ui/assets/{pegDiagram-2B236MQR-C4DWmrja.js → pegDiagram-2B236MQR-GM70woGy.js} +1 -1
- package/dist/ui/assets/{perl-B0DjfKoO.js → perl-BSNM2Kt8.js} +1 -1
- package/dist/ui/assets/{pf-Aor6NyYJ.js → pf-Z-hKf_88.js} +1 -1
- package/dist/ui/assets/{pgsql-CVKcjjNR.js → pgsql-DDgznxBg.js} +1 -1
- package/dist/ui/assets/{php-CZ7geHZt.js → php-BzAhxF-l.js} +1 -1
- package/dist/ui/assets/{php-template-DiFuW2dF.js → php-template-DkgcKHRc.js} +1 -1
- package/dist/ui/assets/{pieDiagram-ENE6RG2P-BO7AmF5j.js → pieDiagram-ENE6RG2P-Bst7mPyz.js} +1 -1
- package/dist/ui/assets/{plaintext-DcvmOuNc.js → plaintext-BYEppD0R.js} +1 -1
- package/dist/ui/assets/{pony-DGPeAcOc.js → pony-BT_kQd1K.js} +1 -1
- package/dist/ui/assets/{powershell-DsrPyJdK.js → powershell-BfRsuSsf.js} +1 -1
- package/dist/ui/assets/{processing-Co2VRXN1.js → processing-DpzWe5Iu.js} +1 -1
- package/dist/ui/assets/{profile-D4PjpLUf.js → profile-BCOhS5cM.js} +1 -1
- package/dist/ui/assets/{prolog-DEbkeg9r.js → prolog-OxaCCaW8.js} +1 -1
- package/dist/ui/assets/{properties-Cxqcuj1s.js → properties-DMFlA2Ca.js} +1 -1
- package/dist/ui/assets/{protobuf-C-zdlrww.js → protobuf-CRDFkXvZ.js} +1 -1
- package/dist/ui/assets/{puppet-BPX3SrOx.js → puppet-DnrVyLzx.js} +1 -1
- package/dist/ui/assets/{purebasic-BdQnNJS3.js → purebasic-Dht4Hniv.js} +1 -1
- package/dist/ui/assets/{python-D067_Gj8.js → python-B0aMirWu.js} +1 -1
- package/dist/ui/assets/{python-repl-DCqNVlc0.js → python-repl-BlYDBGa7.js} +1 -1
- package/dist/ui/assets/{q-kABXYW-8.js → q-DzrtW9kT.js} +1 -1
- package/dist/ui/assets/{qml-Iao-Whg7.js → qml-DnfQsR3i.js} +1 -1
- package/dist/ui/assets/{quadrantDiagram-ABIIQ3AL-DetQn-0l.js → quadrantDiagram-ABIIQ3AL-DNRYurBT.js} +1 -1
- package/dist/ui/assets/{r-BnwpapnO.js → r-DGI_ZuTD.js} +1 -1
- package/dist/ui/assets/{railroadDiagram-RFXS5EU6-DFaFKHAH.js → railroadDiagram-RFXS5EU6-CmbC3Vz4.js} +1 -1
- package/dist/ui/assets/{reasonml-Cn_Y03uT.js → reasonml-3SohMgh2.js} +1 -1
- package/dist/ui/assets/{requirementDiagram-TGXJPOKE-Dg1LrT-T.js → requirementDiagram-TGXJPOKE-8CxrXHco.js} +1 -1
- package/dist/ui/assets/{rib-ByAqQn0v.js → rib-DfRSd4Qa.js} +1 -1
- package/dist/ui/assets/{roboconf-BJPu6NC5.js → roboconf-CQczGdGW.js} +1 -1
- package/dist/ui/assets/{routeros-CL3Jm0Zq.js → routeros-DCka-T2i.js} +1 -1
- package/dist/ui/assets/{rsl-DaXy7KaD.js → rsl-Dr8MOwMo.js} +1 -1
- package/dist/ui/assets/{ruby-DI6awte_.js → ruby-C2zFSkh_.js} +1 -1
- package/dist/ui/assets/{ruleslanguage-DoTVEZE9.js → ruleslanguage-C97YsgQq.js} +1 -1
- package/dist/ui/assets/{rust-ULEd75wO.js → rust-H3h8kd9A.js} +1 -1
- package/dist/ui/assets/{sankeyDiagram-HTMAVEWB-DQtFl110.js → sankeyDiagram-HTMAVEWB-DBXLWjEq.js} +1 -1
- package/dist/ui/assets/{sas-f7ATFuJk.js → sas-04z0Xu8P.js} +1 -1
- package/dist/ui/assets/{scala-Dha1zro2.js → scala-DyB9Aoiv.js} +1 -1
- package/dist/ui/assets/{scheme-BcaFHKeF.js → scheme-DBj6nxUi.js} +1 -1
- package/dist/ui/assets/{scilab-CIiiyD4z.js → scilab-Btj7AD9s.js} +1 -1
- package/dist/ui/assets/{scss-CkBomL0n.js → scss-BXZJyXt7.js} +1 -1
- package/dist/ui/assets/{sequenceDiagram-DBY2YBRQ-DIbNQtCn.js → sequenceDiagram-DBY2YBRQ-DXIkBFKC.js} +1 -1
- package/dist/ui/assets/{shell-Cxu8Py1z.js → shell-BsreQR1Z.js} +1 -1
- package/dist/ui/assets/{sizeCapture-X5ZJPWSS-Cj7PglQv.js → sizeCapture-X5ZJPWSS-ocS9jxAQ.js} +1 -1
- package/dist/ui/assets/{smali-C4EYyWYz.js → smali-D9wQw9zm.js} +1 -1
- package/dist/ui/assets/{smalltalk-BlPTprP4.js → smalltalk-Dku6G_H3.js} +1 -1
- package/dist/ui/assets/{sml-DEPzhlr4.js → sml-BknyU6ya.js} +1 -1
- package/dist/ui/assets/{sqf-CRu3asu9.js → sqf-DRYMaOg8.js} +1 -1
- package/dist/ui/assets/{sql-DK-YfQqg.js → sql-Cw3Ax4KA.js} +1 -1
- package/dist/ui/assets/{sql_more-BuMEOcjj.js → sql_more-vWdslLE0.js} +1 -1
- package/dist/ui/assets/{stan-DMr13kyD.js → stan-8XH5bDS6.js} +1 -1
- package/dist/ui/assets/{stata-Bt6E2zQi.js → stata-4e77wgfe.js} +1 -1
- package/dist/ui/assets/{stateDiagram-2N3HPSRC-Bk-gF72D.js → stateDiagram-2N3HPSRC-RMmL9jMG.js} +1 -1
- package/dist/ui/assets/stateDiagram-v2-6OUMAXLB-QvaEO5U6.js +1 -0
- package/dist/ui/assets/{step21-Bi954ur4.js → step21-ByIeDMY3.js} +1 -1
- package/dist/ui/assets/{stylus-CQwwMaLm.js → stylus-ZAhJyWsw.js} +1 -1
- package/dist/ui/assets/{subunit-BkixDEMH.js → subunit-pPW2UGkj.js} +1 -1
- package/dist/ui/assets/{swift-DS1DWNAE.js → swift-CX3BB5az.js} +1 -1
- package/dist/ui/assets/{swimlanes-5IMT3BWC-C-AZKO5z.js → swimlanes-5IMT3BWC-C2zAuJ56.js} +2 -2
- package/dist/ui/assets/swimlanesDiagram-G3AALYLV-DPgCOY0D.js +8 -0
- package/dist/ui/assets/{taggerscript-DZHi1hoc.js → taggerscript-BHcjbWIC.js} +1 -1
- package/dist/ui/assets/{tap-CXxSD8bM.js → tap-BOl1lBlS.js} +1 -1
- package/dist/ui/assets/{tcl-CdoAAMbn.js → tcl-DZjeOBDh.js} +1 -1
- package/dist/ui/assets/{thrift-DM086HVt.js → thrift-BxboWg4x.js} +1 -1
- package/dist/ui/assets/{timeline-definition-FHXFAJF6-DcrcjFwq.js → timeline-definition-FHXFAJF6-BOxsxyq_.js} +1 -1
- package/dist/ui/assets/{tp-Y-w3S3ES.js → tp-BNM6ql3J.js} +1 -1
- package/dist/ui/assets/{twig-C5c_nAus.js → twig-Dj9ZiXD4.js} +1 -1
- package/dist/ui/assets/{typescript-fc7SMhJu.js → typescript-7MEJlu_6.js} +1 -1
- package/dist/ui/assets/{vala-BV-GlwLI.js → vala-DNqOAgHJ.js} +1 -1
- package/dist/ui/assets/{vbnet-BIqQmnKr.js → vbnet-T0mNKVc_.js} +1 -1
- package/dist/ui/assets/{vbscript-h186Lnrz.js → vbscript-CeuwVMAI.js} +1 -1
- package/dist/ui/assets/{vbscript-html-DcuP7lTb.js → vbscript-html-BbJSt2w6.js} +1 -1
- package/dist/ui/assets/{vennDiagram-L72KCM5P-Dnh11y2e.js → vennDiagram-L72KCM5P-BX9mWeiT.js} +1 -1
- package/dist/ui/assets/{verilog-Cbgb_XnR.js → verilog-DYuhYfkX.js} +1 -1
- package/dist/ui/assets/{vhdl-COP7w5IC.js → vhdl-Cf9aT-t7.js} +1 -1
- package/dist/ui/assets/{vim-COGwKzgS.js → vim-DciP44K1.js} +1 -1
- package/dist/ui/assets/{wardleyDiagram-EHGQE667-BeiD8fPz.js → wardleyDiagram-EHGQE667-CDj2rOzu.js} +1 -1
- package/dist/ui/assets/{x86asm-BdBqPPf4.js → x86asm-E-AN_LXO.js} +1 -1
- package/dist/ui/assets/{xl-BmCQT7Vj.js → xl-CxYLJelN.js} +1 -1
- package/dist/ui/assets/{xml-DM0NxGiJ.js → xml-yz1SRLuU.js} +1 -1
- package/dist/ui/assets/{xquery-BUaYHDJM.js → xquery-BLzBF3tV.js} +1 -1
- package/dist/ui/assets/{xychartDiagram-FW5EYKEG-CnwvTrkm.js → xychartDiagram-FW5EYKEG-Czapk9-Z.js} +1 -1
- package/dist/ui/assets/{yaml-DMs8lGDt.js → yaml-Cv4Y7iN_.js} +1 -1
- package/dist/ui/assets/{zephir-BvjoPdmg.js → zephir-KRcdQfEX.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/utils/__tests__/knowledge-store.test.js +86 -5
- package/dist/utils/__tests__/pi-browser-tools.test.d.ts +1 -0
- package/dist/utils/__tests__/pi-browser-tools.test.js +30 -0
- package/dist/utils/__tests__/pi-crawl-tools.test.d.ts +1 -0
- package/dist/utils/__tests__/pi-crawl-tools.test.js +315 -0
- package/dist/utils/__tests__/pi-dedup-reconcile.test.d.ts +1 -0
- package/dist/utils/__tests__/pi-dedup-reconcile.test.js +89 -0
- package/dist/utils/__tests__/pi-dedup.test.d.ts +1 -0
- package/dist/utils/__tests__/pi-dedup.test.js +106 -0
- package/dist/utils/__tests__/pi-learn-orchestrator.test.d.ts +1 -0
- package/dist/utils/__tests__/pi-learn-orchestrator.test.js +885 -0
- package/dist/utils/__tests__/pi-runtime-json.test.d.ts +1 -0
- package/dist/utils/__tests__/pi-runtime-json.test.js +11 -0
- package/dist/utils/__tests__/web-crawler.test.d.ts +1 -0
- package/dist/utils/__tests__/web-crawler.test.js +69 -0
- package/dist/utils/db.d.ts +1 -0
- package/dist/utils/db.js +18 -0
- package/dist/utils/knowledge-store.d.ts +9 -0
- package/dist/utils/knowledge-store.js +85 -11
- package/dist/utils/pi-browser-tools.d.ts +2 -0
- package/dist/utils/pi-browser-tools.js +9 -2
- package/dist/utils/pi-crawl-tools.d.ts +62 -0
- package/dist/utils/pi-crawl-tools.js +298 -0
- package/dist/utils/pi-dedup.d.ts +62 -0
- package/dist/utils/pi-dedup.js +218 -0
- package/dist/utils/pi-learn-orchestrator.d.ts +44 -0
- package/dist/utils/pi-learn-orchestrator.js +431 -0
- package/dist/utils/pi-runtime.d.ts +23 -1
- package/dist/utils/pi-runtime.js +255 -68
- package/dist/utils/request-writer.d.ts +1 -0
- package/dist/utils/request-writer.js +5 -4
- package/dist/utils/web-crawler.d.ts +13 -0
- package/dist/utils/web-crawler.js +64 -0
- package/package.json +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.CHyuB7c8.js +0 -1
- package/dist/ui/assets/channel-BdcV3Ykv.js +0 -1
- package/dist/ui/assets/classDiagram-OUVF2IWQ-CAZR5rAv.js +0 -1
- package/dist/ui/assets/classDiagram-v2-EOCWNBFH-CAZR5rAv.js +0 -1
- package/dist/ui/assets/index-WPLwx8h5.css +0 -10
- package/dist/ui/assets/stateDiagram-v2-6OUMAXLB-BFJCYsLY.js +0 -1
- package/dist/ui/assets/swimlanesDiagram-G3AALYLV-BZd1Gt4_.js +0 -8
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
import { promises as fsp } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
// Mock pi-runtime: 每个测试通过 mockImplementationOnce 控制 createPiSession 的返回
|
|
5
|
+
jest.mock('../pi-runtime.js', () => ({
|
|
6
|
+
createPiSession: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
// Mock pi-dedup:只 mock 阻断 + LLM 裁定 + mintSlug;其余(mergeTopics/readAllTopics 等)保留真实实现
|
|
9
|
+
jest.mock('../pi-dedup.js', () => {
|
|
10
|
+
const actual = jest.requireActual('../pi-dedup.js');
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
assertNoOldDirs: jest.fn(),
|
|
14
|
+
reconcileTopics: jest.fn(async () => ({ match: false })),
|
|
15
|
+
mintSlug: jest.fn(),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
// Mock pi-crawl-tools:url 模式的 CrawlGuard 替身(fixture graph,saveSource 落真实 tmpDir)。
|
|
19
|
+
// 图为闭包共享变量,用例通过 __setGraph 配置。
|
|
20
|
+
jest.mock('../pi-crawl-tools.js', () => {
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const p = require('node:path');
|
|
23
|
+
let graph = {};
|
|
24
|
+
// 在构造函数 this 上初始化(jest mock.instances 才能拿到带状态的实例)
|
|
25
|
+
const FakeCrawlGuard = jest.fn(function (gopts) {
|
|
26
|
+
let saved = 0;
|
|
27
|
+
this.gopts = gopts;
|
|
28
|
+
this.closed = false;
|
|
29
|
+
this.open = async () => {
|
|
30
|
+
const page = graph[gopts.entryUrl];
|
|
31
|
+
if (!page)
|
|
32
|
+
throw new Error(`fake guard fixture missing: ${gopts.entryUrl}`);
|
|
33
|
+
return { url: gopts.entryUrl, ...page };
|
|
34
|
+
};
|
|
35
|
+
this.visit = async (url) => {
|
|
36
|
+
const page = graph[url];
|
|
37
|
+
if (!page)
|
|
38
|
+
throw new Error(`fake guard fixture missing: ${url}`);
|
|
39
|
+
return { url, ...page };
|
|
40
|
+
};
|
|
41
|
+
this.saveSource = (url) => {
|
|
42
|
+
const page = graph[url];
|
|
43
|
+
if (!page)
|
|
44
|
+
throw new Error(`fake guard save missing: ${url}`);
|
|
45
|
+
const dir = p.join(gopts.docsDir, gopts.slug, 'sources');
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
const name = `source-${String(saved + 1).padStart(3, '0')}.md`;
|
|
48
|
+
fs.writeFileSync(p.join(dir, name), `Source: ${url}\n\n${page.text}`, 'utf8');
|
|
49
|
+
saved += 1;
|
|
50
|
+
return saved;
|
|
51
|
+
};
|
|
52
|
+
this.close = async () => { this.closed = true; };
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
CrawlGuard: FakeCrawlGuard,
|
|
56
|
+
createCrawlTools: jest.fn(() => []),
|
|
57
|
+
/** 测试用:替换 fixture graph(所有后续新建 guard 生效) */
|
|
58
|
+
__setGraph: (g) => { graph = g; },
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
import { createPiSession } from '../pi-runtime.js';
|
|
62
|
+
import { reconcileTopics, assertNoOldDirs, mintSlug } from '../pi-dedup.js';
|
|
63
|
+
import { runLearnPipeline } from '../pi-learn-orchestrator.js';
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
65
|
+
const crawlToolsMock = require('../pi-crawl-tools.js');
|
|
66
|
+
const mockCreatePiSession = createPiSession;
|
|
67
|
+
const mockReconcile = reconcileTopics;
|
|
68
|
+
const mockAssertNoOld = assertNoOldDirs;
|
|
69
|
+
const mockMintSlug = mintSlug;
|
|
70
|
+
class FakeSession {
|
|
71
|
+
constructor(handler) {
|
|
72
|
+
Object.defineProperty(this, "handler", {
|
|
73
|
+
enumerable: true,
|
|
74
|
+
configurable: true,
|
|
75
|
+
writable: true,
|
|
76
|
+
value: handler
|
|
77
|
+
});
|
|
78
|
+
Object.defineProperty(this, "sessionId", {
|
|
79
|
+
enumerable: true,
|
|
80
|
+
configurable: true,
|
|
81
|
+
writable: true,
|
|
82
|
+
value: `fake-${Math.random().toString(36).slice(2)}`
|
|
83
|
+
});
|
|
84
|
+
Object.defineProperty(this, "listeners", {
|
|
85
|
+
enumerable: true,
|
|
86
|
+
configurable: true,
|
|
87
|
+
writable: true,
|
|
88
|
+
value: new Set()
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
subscribe(fn) {
|
|
92
|
+
this.listeners.add(fn);
|
|
93
|
+
return () => { this.listeners.delete(fn); };
|
|
94
|
+
}
|
|
95
|
+
async prompt(text) {
|
|
96
|
+
// "轮次" = 一次 assistant 交互(message_start → message_end)
|
|
97
|
+
this.emit({ type: 'message_start', message: { role: 'assistant' } });
|
|
98
|
+
await this.handler(text);
|
|
99
|
+
this.emit({ type: 'message_end', message: { role: 'assistant' } });
|
|
100
|
+
this.emit({ type: 'agent_end', messages: [], willRetry: false });
|
|
101
|
+
}
|
|
102
|
+
async waitForIdle() { }
|
|
103
|
+
async abort() { }
|
|
104
|
+
emit(ev) {
|
|
105
|
+
for (const l of this.listeners)
|
|
106
|
+
l(ev);
|
|
107
|
+
}
|
|
108
|
+
/** 返回一个 handler:模拟 LLM 调了一次 add_topic(在 prompt 内 emit,确保 subscribe 后能收到) */
|
|
109
|
+
static addTopicHandler(tags) {
|
|
110
|
+
return async function () {
|
|
111
|
+
this.emit({
|
|
112
|
+
type: 'tool_execution_start',
|
|
113
|
+
toolCallId: 'call-1',
|
|
114
|
+
toolName: 'add_topic',
|
|
115
|
+
args: { slug: 'x', title: 't', summary: 's', tags },
|
|
116
|
+
});
|
|
117
|
+
// Finding 1:成功判定 deferred 到 tool_execution_end 且 isError=false
|
|
118
|
+
this.emit({
|
|
119
|
+
type: 'tool_execution_end',
|
|
120
|
+
toolCallId: 'call-1',
|
|
121
|
+
toolName: 'add_topic',
|
|
122
|
+
isError: false,
|
|
123
|
+
durationMs: 12,
|
|
124
|
+
result: { content: [{ type: 'text', text: 'ok' }] },
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** 包装一个 S2 handler:先 emit read_source 成功(S2 readSourceOk 判定),再跑 inner */
|
|
129
|
+
static readSourceThen(inner) {
|
|
130
|
+
return async function (prompt) {
|
|
131
|
+
this.emit({
|
|
132
|
+
type: 'tool_execution_start',
|
|
133
|
+
toolCallId: 'read-1',
|
|
134
|
+
toolName: 'read_source',
|
|
135
|
+
args: { topic: 'x' },
|
|
136
|
+
});
|
|
137
|
+
this.emit({
|
|
138
|
+
type: 'tool_execution_end',
|
|
139
|
+
toolCallId: 'read-1',
|
|
140
|
+
toolName: 'read_source',
|
|
141
|
+
isError: false,
|
|
142
|
+
durationMs: 3,
|
|
143
|
+
result: { content: [{ type: 'text', text: 'sources' }] },
|
|
144
|
+
});
|
|
145
|
+
await inner.call(this, prompt);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** add_topic 带非空 tags 但工具执行失败(upsert 抛错等) → orchestrator 必须重试 */
|
|
149
|
+
static addTopicErrorHandler(tags) {
|
|
150
|
+
return async function () {
|
|
151
|
+
this.emit({
|
|
152
|
+
type: 'tool_execution_start',
|
|
153
|
+
toolCallId: 'call-1',
|
|
154
|
+
toolName: 'add_topic',
|
|
155
|
+
args: { slug: 'x', title: 't', summary: 's', tags },
|
|
156
|
+
});
|
|
157
|
+
this.emit({
|
|
158
|
+
type: 'tool_execution_end',
|
|
159
|
+
toolCallId: 'call-1',
|
|
160
|
+
toolName: 'add_topic',
|
|
161
|
+
isError: true,
|
|
162
|
+
durationMs: 5,
|
|
163
|
+
result: { content: [{ type: 'text', text: 'upsertTopic failed: invalid slug' }] },
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function makeEvents() {
|
|
169
|
+
const calls = [];
|
|
170
|
+
const ev = {
|
|
171
|
+
calls,
|
|
172
|
+
stageStart: (stage, totalItems) => calls.push(`stageStart:${stage}${totalItems !== undefined ? ':' + totalItems : ''}`),
|
|
173
|
+
stageItemStart: (i, _t, title) => calls.push(`stageItemStart:${i}:${title}`),
|
|
174
|
+
stageItemEnd: (i, chunkId) => calls.push(`stageItemEnd:${i}:${chunkId}`),
|
|
175
|
+
stageItemFailed: (i, _title, reason) => calls.push(`stageItemFailed:${i}:${reason}`),
|
|
176
|
+
stageEnd: (stage) => calls.push(`stageEnd:${stage}`),
|
|
177
|
+
roundStart: (i) => calls.push(`roundStart:${i}`),
|
|
178
|
+
roundEnd: (i) => calls.push(`roundEnd:${i}`),
|
|
179
|
+
textDelta: () => { },
|
|
180
|
+
thinkingDelta: () => { },
|
|
181
|
+
toolStart: () => { },
|
|
182
|
+
toolEnd: () => { },
|
|
183
|
+
};
|
|
184
|
+
return ev;
|
|
185
|
+
}
|
|
186
|
+
// ---------- 默认 session 路由(url 用例用;Once / 显式 implementation 优先) ----------
|
|
187
|
+
/** crawl handler:open → save_source → 沿 links 逐页 visit + save_source */
|
|
188
|
+
function makeCrawlHandler(guard) {
|
|
189
|
+
return async function () {
|
|
190
|
+
const entry = await guard.open();
|
|
191
|
+
guard.saveSource(entry.url);
|
|
192
|
+
for (const link of entry.links ?? []) {
|
|
193
|
+
const child = await guard.visit(link);
|
|
194
|
+
guard.saveSource(child.url);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** outline handler:从 prompt 解析 slug,写 outline.json + seed index.json(1 item) */
|
|
199
|
+
function defaultOutlineHandler() {
|
|
200
|
+
return async function (prompt) {
|
|
201
|
+
const slug = prompt.match(/slug: ([^\s\n]+)/)[1];
|
|
202
|
+
await fsp.mkdir(path.join(tmpDir, slug, 'chunks'), { recursive: true });
|
|
203
|
+
const items = [{ id: 'a', title: '概念A', oneLine: 'aaa' }];
|
|
204
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'outline.json'), JSON.stringify({ slug, source: 'sources/', items }, null, 2));
|
|
205
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'index.json'), JSON.stringify({ slug, title: 'Default', summary: '(refining)', tags: [], chunks: [
|
|
206
|
+
{ id: 'chunk-001', title: '概念A', summary: 'aaa', tags: [] },
|
|
207
|
+
] }, null, 2));
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/** 门1 用 S1 handler:写 outline.json + seed index,chunks 必须为 []——
|
|
211
|
+
* 否则真实 mergeTopics 会按 ref 去找不存在的 chunk 正文而抛错 */
|
|
212
|
+
function gate1OutlineHandler(slug, items) {
|
|
213
|
+
return async function () {
|
|
214
|
+
await fsp.mkdir(path.join(tmpDir, slug, 'chunks'), { recursive: true });
|
|
215
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'outline.json'), JSON.stringify({ slug, source: 'sources/', items }, null, 2));
|
|
216
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'index.json'), JSON.stringify({ slug, title: 'New', summary: '(refining)', tags: [], chunks: [] }, null, 2));
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** 预先铺一个已有主题 dstSlug(1 chunk)+ 根 index 注册 */
|
|
220
|
+
async function seedOldTopic(dstSlug) {
|
|
221
|
+
await fsp.mkdir(path.join(tmpDir, dstSlug, 'chunks'), { recursive: true });
|
|
222
|
+
await fsp.writeFile(path.join(tmpDir, dstSlug, 'index.json'), JSON.stringify({ slug: dstSlug, title: 'Old', summary: 'old summary', tags: [], chunks: [
|
|
223
|
+
{ id: 'chunk-001', title: 'T1', summary: 's1', tags: [] },
|
|
224
|
+
] }, null, 2));
|
|
225
|
+
await fsp.writeFile(path.join(tmpDir, dstSlug, 'chunks', 'chunk-001.md'), '# T1');
|
|
226
|
+
await fsp.writeFile(path.join(tmpDir, 'index.json'), JSON.stringify({ topics: [
|
|
227
|
+
{ slug: dstSlug, title: 'Old', summary: 'old summary', tags: [] },
|
|
228
|
+
] }, null, 2));
|
|
229
|
+
}
|
|
230
|
+
/** 门0 用 S1 handler(file 模式):旧 topic 已旁挂,fresh dir 由 file 阶段自己写 sources;
|
|
231
|
+
* outline session 写 outline.json + seed index.json,chunks 必须为 [](镜像 gate1OutlineHandler) */
|
|
232
|
+
function gate0OutlineHandler(slug, items) {
|
|
233
|
+
return async function () {
|
|
234
|
+
await fsp.mkdir(path.join(tmpDir, slug, 'chunks'), { recursive: true });
|
|
235
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'outline.json'), JSON.stringify({ slug, source: 'sources/', items }, null, 2));
|
|
236
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'index.json'), JSON.stringify({ slug, title: 'New', summary: '(refining)', tags: [], chunks: [] }, null, 2));
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** 门0 用例:预铺 slug=dup 的旧主题(sources + 2 chunks + index + 根 index 登记) */
|
|
240
|
+
async function seedDupTopic() {
|
|
241
|
+
await fsp.mkdir(path.join(tmpDir, 'dup', 'chunks'), { recursive: true });
|
|
242
|
+
await fsp.mkdir(path.join(tmpDir, 'dup', 'sources'), { recursive: true });
|
|
243
|
+
await fsp.writeFile(path.join(tmpDir, 'dup', 'index.json'), JSON.stringify({ slug: 'dup', title: 'Old Dup', summary: 'old dup summary', tags: [], chunks: [
|
|
244
|
+
{ id: 'chunk-001', title: 'T1', summary: 's1', tags: [] },
|
|
245
|
+
{ id: 'chunk-002', title: 'T2', summary: 's2', tags: [] },
|
|
246
|
+
] }, null, 2));
|
|
247
|
+
await fsp.writeFile(path.join(tmpDir, 'dup', 'chunks', 'chunk-001.md'), 'OLD BODY 001\n');
|
|
248
|
+
await fsp.writeFile(path.join(tmpDir, 'dup', 'chunks', 'chunk-002.md'), 'OLD BODY 002\n');
|
|
249
|
+
await fsp.writeFile(path.join(tmpDir, 'dup', 'sources', 'source-001.md'), 'OLD SOURCE\n');
|
|
250
|
+
await fsp.writeFile(path.join(tmpDir, 'index.json'), JSON.stringify({ topics: [
|
|
251
|
+
{ slug: 'dup', title: 'Old Dup', summary: 'old dup summary', tags: [] },
|
|
252
|
+
] }, null, 2));
|
|
253
|
+
}
|
|
254
|
+
/** tag handler:先捕获 prompt 文本(断言 tag 对哪个 slug / 用了什么 title),再 emit add_topic */
|
|
255
|
+
function tagCaptureHandler(captured) {
|
|
256
|
+
const add = FakeSession.addTopicHandler(['merged-tag']);
|
|
257
|
+
return async function (prompt) {
|
|
258
|
+
captured.push(prompt);
|
|
259
|
+
await add.call(this, prompt);
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// ---------- Test scaffolding ----------
|
|
263
|
+
let tmpDir;
|
|
264
|
+
beforeEach(async () => {
|
|
265
|
+
tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'learn-pipe-'));
|
|
266
|
+
mockCreatePiSession.mockReset();
|
|
267
|
+
// 查重门 mock:默认两道门都不合并,保证现有用例行为不变
|
|
268
|
+
mockReconcile.mockReset();
|
|
269
|
+
mockReconcile.mockResolvedValue({ match: false });
|
|
270
|
+
mockAssertNoOld.mockReset();
|
|
271
|
+
mockAssertNoOld.mockImplementation(() => { });
|
|
272
|
+
// mintSlug 默认另铸固定 slug;门0 碰撞用例可覆盖
|
|
273
|
+
mockMintSlug.mockReset();
|
|
274
|
+
mockMintSlug.mockResolvedValue('minted-slug');
|
|
275
|
+
// CrawlGuard 的 mock 由 jest.mock 工厂创建,生命周期跨用例,需手动清调用记录
|
|
276
|
+
crawlToolsMock.CrawlGuard.mockClear();
|
|
277
|
+
// 默认按 mode 路由;具体用例的 mockImplementationOnce 优先
|
|
278
|
+
mockCreatePiSession.mockImplementation(async (options) => {
|
|
279
|
+
if (options.mode === 'crawl') {
|
|
280
|
+
return new FakeSession(makeCrawlHandler(options.crawlGuard));
|
|
281
|
+
}
|
|
282
|
+
if (options.mode === 'outline') {
|
|
283
|
+
return new FakeSession(defaultOutlineHandler());
|
|
284
|
+
}
|
|
285
|
+
if (options.mode === 'refine') {
|
|
286
|
+
return new FakeSession(FakeSession.readSourceThen(async () => {
|
|
287
|
+
const slug = 'pi';
|
|
288
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'chunks', 'chunk-001.md'), '# 概念A');
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
return new FakeSession(FakeSession.addTopicHandler(['tag-a', 'tag-b']));
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
afterEach(async () => {
|
|
295
|
+
await fsp.rm(tmpDir, { recursive: true, force: true });
|
|
296
|
+
});
|
|
297
|
+
// ============================================================
|
|
298
|
+
describe('pi-learn-orchestrator', () => {
|
|
299
|
+
test('S1 失败:session 从不写 outline.json → reject 含「outline 未生成」', async () => {
|
|
300
|
+
mockCreatePiSession.mockImplementation(async () => {
|
|
301
|
+
return new FakeSession(async () => { });
|
|
302
|
+
});
|
|
303
|
+
await expect(runLearnPipeline({
|
|
304
|
+
sourceType: 'file',
|
|
305
|
+
input: '# body',
|
|
306
|
+
label: 'my-topic.md',
|
|
307
|
+
port: 9999,
|
|
308
|
+
events: makeEvents(),
|
|
309
|
+
docsDir: tmpDir,
|
|
310
|
+
})).rejects.toThrow(/outline 未生成/);
|
|
311
|
+
expect(mockCreatePiSession).toHaveBeenCalledTimes(1);
|
|
312
|
+
expect(mockCreatePiSession.mock.calls[0][0].mode).toBe('outline');
|
|
313
|
+
});
|
|
314
|
+
test('S2 部分失败:3 items,第 2 个重试也失败 → chunksWritten=2, itemsFailed=1', async () => {
|
|
315
|
+
const items = [
|
|
316
|
+
{ id: 'a', title: '概念A', oneLine: 'aaa' },
|
|
317
|
+
{ id: 'b', title: '概念B', oneLine: 'bbb' },
|
|
318
|
+
{ id: 'c', title: '概念C', oneLine: 'ccc' },
|
|
319
|
+
];
|
|
320
|
+
const slug = 'my-topic';
|
|
321
|
+
// S1 outline session: 写 outline.json + seed index.json(真实流程由 add_outline 工具做)。
|
|
322
|
+
// 必须在 session handler 内做:pipeline 开头的 prepareTopicDir 会清空重建 topic 目录。
|
|
323
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
324
|
+
return new FakeSession(async () => {
|
|
325
|
+
await fsp.mkdir(path.join(tmpDir, slug, 'chunks'), { recursive: true });
|
|
326
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'outline.json'), JSON.stringify({ slug, source: 'sources/', items }, null, 2));
|
|
327
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'index.json'), JSON.stringify({ slug, title: 'My Topic', summary: '(refining)', tags: [], chunks: [
|
|
328
|
+
{ id: 'chunk-001', title: '概念A', summary: 'aaa', tags: [] },
|
|
329
|
+
{ id: 'chunk-003', title: '概念C', summary: 'ccc', tags: [] },
|
|
330
|
+
] }, null, 2));
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
// S2 调用序列:item0 try0 (写 chunk-001), item1 try0 (空), item1 try1 (空), item2 try0 (写 chunk-003)
|
|
334
|
+
const writeChunk = async (i) => {
|
|
335
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'chunks', `chunk-00${i + 1}.md`), `# 概念${'ABC'[i]}`);
|
|
336
|
+
};
|
|
337
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => { await writeChunk(0); })));
|
|
338
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => { })));
|
|
339
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => { })));
|
|
340
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => { await writeChunk(2); })));
|
|
341
|
+
// S3: 兜底 mock(mockImplementationOnce 已用完),handler 内 emit add_topic
|
|
342
|
+
mockCreatePiSession.mockImplementation(async () => {
|
|
343
|
+
return new FakeSession(FakeSession.addTopicHandler(['react', 'hooks']));
|
|
344
|
+
});
|
|
345
|
+
const events = makeEvents();
|
|
346
|
+
const result = await runLearnPipeline({
|
|
347
|
+
sourceType: 'file',
|
|
348
|
+
input: '# body',
|
|
349
|
+
label: `${slug}.md`,
|
|
350
|
+
port: 9999,
|
|
351
|
+
events,
|
|
352
|
+
docsDir: tmpDir,
|
|
353
|
+
});
|
|
354
|
+
expect(result.slug).toBe('my-topic');
|
|
355
|
+
expect(result.chunksWritten).toBe(2);
|
|
356
|
+
expect(result.itemsFailed).toBe(1);
|
|
357
|
+
const failed = events.calls.filter((c) => c.startsWith('stageItemFailed:'));
|
|
358
|
+
expect(failed).toHaveLength(1);
|
|
359
|
+
expect(failed[0]).toContain('chunk 文件未生成');
|
|
360
|
+
expect(failed[0]).toContain('stageItemFailed:1:');
|
|
361
|
+
});
|
|
362
|
+
test('S3 happy path:add_topic 被调用且 tags 非空 → stageEnd(tag)', async () => {
|
|
363
|
+
const items = [{ id: 'a', title: '概念A', oneLine: 'aaa' }];
|
|
364
|
+
const slug = 'happy-topic';
|
|
365
|
+
// S1: session 写 outline.json,并 seed index.json + chunk(真实流程由 add_outline / add_chunk 工具写)
|
|
366
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
367
|
+
return new FakeSession(async () => {
|
|
368
|
+
await fsp.mkdir(path.join(tmpDir, slug, 'chunks'), { recursive: true });
|
|
369
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'outline.json'), JSON.stringify({ slug, source: 'sources/', items }, null, 2));
|
|
370
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'index.json'), JSON.stringify({ slug, title: 'Happy', summary: '(refining)', tags: [], chunks: [
|
|
371
|
+
{ id: 'chunk-001', title: '概念A', summary: 'aaa', tags: [] },
|
|
372
|
+
] }, null, 2));
|
|
373
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'chunks', 'chunk-001.md'), '# 概念A');
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
// S2 item0: chunk-001.md 已存在,session 仅 emit read_source 成功
|
|
377
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => { })));
|
|
378
|
+
// S3: handler 内 emit add_topic 带非空 tags
|
|
379
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
380
|
+
return new FakeSession(FakeSession.addTopicHandler(['synonym-a', 'hypernym-b']));
|
|
381
|
+
});
|
|
382
|
+
const events = makeEvents();
|
|
383
|
+
const result = await runLearnPipeline({
|
|
384
|
+
sourceType: 'file',
|
|
385
|
+
input: '# body',
|
|
386
|
+
label: `${slug}.md`,
|
|
387
|
+
port: 9999,
|
|
388
|
+
events,
|
|
389
|
+
docsDir: tmpDir,
|
|
390
|
+
});
|
|
391
|
+
expect(result.slug).toBe(slug);
|
|
392
|
+
expect(events.calls).toContain('stageEnd:outline');
|
|
393
|
+
expect(events.calls).toContain('stageEnd:refine');
|
|
394
|
+
expect(events.calls).toContain('stageEnd:tag');
|
|
395
|
+
});
|
|
396
|
+
test('S1 outline.json parse 失败 → stageEnd(outline) 仍发出,再 throw', async () => {
|
|
397
|
+
const slug = 'broken-topic';
|
|
398
|
+
// S1 session 写一个非法 JSON 的 outline.json(prepareTopicDir 已先清空目录,故须在 handler 内写)
|
|
399
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(async () => {
|
|
400
|
+
await fsp.mkdir(path.join(tmpDir, slug, 'chunks'), { recursive: true });
|
|
401
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'outline.json'), '{not valid json', 'utf8');
|
|
402
|
+
}));
|
|
403
|
+
const events = makeEvents();
|
|
404
|
+
await expect(runLearnPipeline({
|
|
405
|
+
sourceType: 'file',
|
|
406
|
+
input: '# body',
|
|
407
|
+
label: `${slug}.md`,
|
|
408
|
+
port: 9999,
|
|
409
|
+
events,
|
|
410
|
+
docsDir: tmpDir,
|
|
411
|
+
})).rejects.toThrow();
|
|
412
|
+
// stageEnd 必须在 throw 之前发出
|
|
413
|
+
expect(events.calls).toContain('stageEnd:outline');
|
|
414
|
+
});
|
|
415
|
+
test('S2 createPiSession 抛错 → stageItemFailed 发出,pipeline 继续', async () => {
|
|
416
|
+
const items = [
|
|
417
|
+
{ id: 'a', title: '概念A', oneLine: 'aaa' },
|
|
418
|
+
{ id: 'b', title: '概念B', oneLine: 'bbb' },
|
|
419
|
+
];
|
|
420
|
+
const slug = 'err-topic';
|
|
421
|
+
// S1: 写 outline.json + seed index.json
|
|
422
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
423
|
+
return new FakeSession(async () => {
|
|
424
|
+
await fsp.mkdir(path.join(tmpDir, slug, 'chunks'), { recursive: true });
|
|
425
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'outline.json'), JSON.stringify({ slug, source: 'sources/', items }, null, 2));
|
|
426
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'index.json'), JSON.stringify({ slug, title: 'Err', summary: '(refining)', tags: [], chunks: [
|
|
427
|
+
{ id: 'chunk-002', title: '概念B', summary: 'bbb', tags: [] },
|
|
428
|
+
] }, null, 2));
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
// S2 item0: createPiSession 抛错(模拟 pi runtime 挂了)
|
|
432
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
433
|
+
throw new Error('port down');
|
|
434
|
+
});
|
|
435
|
+
// S2 item1: 成功写 chunk-002.md
|
|
436
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
437
|
+
return new FakeSession(FakeSession.readSourceThen(async () => {
|
|
438
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'chunks', 'chunk-002.md'), '# 概念B');
|
|
439
|
+
}));
|
|
440
|
+
});
|
|
441
|
+
// S3: handler 内 emit add_topic
|
|
442
|
+
mockCreatePiSession.mockImplementation(async () => {
|
|
443
|
+
return new FakeSession(FakeSession.addTopicHandler(['tag1']));
|
|
444
|
+
});
|
|
445
|
+
const events = makeEvents();
|
|
446
|
+
const result = await runLearnPipeline({
|
|
447
|
+
sourceType: 'file',
|
|
448
|
+
input: '# body',
|
|
449
|
+
label: `${slug}.md`,
|
|
450
|
+
port: 9999,
|
|
451
|
+
events,
|
|
452
|
+
docsDir: tmpDir,
|
|
453
|
+
});
|
|
454
|
+
// item0 失败(port down),item1 成功
|
|
455
|
+
expect(result.itemsFailed).toBe(1);
|
|
456
|
+
expect(result.chunksWritten).toBe(1);
|
|
457
|
+
// stageItemFailed 必须发出,reason 含 port down
|
|
458
|
+
const failed = events.calls.filter((c) => c.startsWith('stageItemFailed:'));
|
|
459
|
+
expect(failed).toHaveLength(1);
|
|
460
|
+
expect(failed[0]).toContain('port down');
|
|
461
|
+
// stageEnd(refine) 必须发出
|
|
462
|
+
expect(events.calls).toContain('stageEnd:refine');
|
|
463
|
+
});
|
|
464
|
+
test('S3 add_topic 带空 tags → 视为失败,重试一次;第二次非空 → 成功', async () => {
|
|
465
|
+
const items = [{ id: 'a', title: '概念A', oneLine: 'aaa' }];
|
|
466
|
+
const slug = 'retry-topic';
|
|
467
|
+
// S1: 写 outline.json + seed index.json + chunk
|
|
468
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
469
|
+
return new FakeSession(async () => {
|
|
470
|
+
await fsp.mkdir(path.join(tmpDir, slug, 'chunks'), { recursive: true });
|
|
471
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'outline.json'), JSON.stringify({ slug, source: 'sources/', items }, null, 2));
|
|
472
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'index.json'), JSON.stringify({ slug, title: 'Retry', summary: '(refining)', tags: [], chunks: [
|
|
473
|
+
{ id: 'chunk-001', title: '概念A', summary: 'aaa', tags: [] },
|
|
474
|
+
] }, null, 2));
|
|
475
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'chunks', 'chunk-001.md'), '# A');
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
// S2 item0: chunk 已存在,仅 read_source 成功
|
|
479
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => { })));
|
|
480
|
+
// S3 try0: add_topic 带空 tags → 不算成功
|
|
481
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
482
|
+
return new FakeSession(FakeSession.addTopicHandler([]));
|
|
483
|
+
});
|
|
484
|
+
// S3 try1: add_topic 带非空 tags → 成功
|
|
485
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
486
|
+
return new FakeSession(FakeSession.addTopicHandler(['real-tag']));
|
|
487
|
+
});
|
|
488
|
+
const events = makeEvents();
|
|
489
|
+
const result = await runLearnPipeline({
|
|
490
|
+
sourceType: 'file',
|
|
491
|
+
input: '# body',
|
|
492
|
+
label: `${slug}.md`,
|
|
493
|
+
port: 9999,
|
|
494
|
+
events,
|
|
495
|
+
docsDir: tmpDir,
|
|
496
|
+
});
|
|
497
|
+
expect(result.slug).toBe(slug);
|
|
498
|
+
expect(events.calls).toContain('stageEnd:tag');
|
|
499
|
+
});
|
|
500
|
+
test('Finding 1:S3 add_topic tags 非空但 tool_execution_end isError=true → 不算成功,重试后成功', async () => {
|
|
501
|
+
const items = [{ id: 'a', title: '概念A', oneLine: 'aaa' }];
|
|
502
|
+
const slug = 'err-tag-topic';
|
|
503
|
+
// S1: 写 outline.json + seed index.json + chunk
|
|
504
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
505
|
+
return new FakeSession(async () => {
|
|
506
|
+
await fsp.mkdir(path.join(tmpDir, slug, 'chunks'), { recursive: true });
|
|
507
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'outline.json'), JSON.stringify({ slug, source: 'sources/', items }, null, 2));
|
|
508
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'index.json'), JSON.stringify({ slug, title: 'T', summary: '(refining)', tags: [], chunks: [
|
|
509
|
+
{ id: 'chunk-001', title: '概念A', summary: 'aaa', tags: [] },
|
|
510
|
+
] }, null, 2));
|
|
511
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'chunks', 'chunk-001.md'), '# A');
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
// S2 item0: chunk 已存在,仅 read_source 成功
|
|
515
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => { })));
|
|
516
|
+
// S3 try0: add_topic 带非空 tags 但工具执行失败 → 必须重试
|
|
517
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
518
|
+
return new FakeSession(FakeSession.addTopicErrorHandler(['bad-tag']));
|
|
519
|
+
});
|
|
520
|
+
// S3 try1: add_topic 成功
|
|
521
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
522
|
+
return new FakeSession(FakeSession.addTopicHandler(['real-tag']));
|
|
523
|
+
});
|
|
524
|
+
const events = makeEvents();
|
|
525
|
+
const result = await runLearnPipeline({
|
|
526
|
+
sourceType: 'file',
|
|
527
|
+
input: '# body',
|
|
528
|
+
label: `${slug}.md`,
|
|
529
|
+
port: 9999,
|
|
530
|
+
events,
|
|
531
|
+
docsDir: tmpDir,
|
|
532
|
+
});
|
|
533
|
+
expect(result.slug).toBe(slug);
|
|
534
|
+
expect(events.calls).toContain('stageEnd:tag');
|
|
535
|
+
// S1(1) + S2(1) + S3 两次重试 = 4 次 session
|
|
536
|
+
expect(mockCreatePiSession).toHaveBeenCalledTimes(4);
|
|
537
|
+
});
|
|
538
|
+
test('Finding 2:S2 item 写了 chunk-001.md 但从未调 add_chunk(index.json 无 chunkId)→ item 失败', async () => {
|
|
539
|
+
const items = [{ id: 'a', title: '概念A', oneLine: 'aaa' }];
|
|
540
|
+
const slug = 'orphan-topic';
|
|
541
|
+
// S1: 写 outline.json + seed index.json(chunks 为空,模拟 add_chunk 没被调)
|
|
542
|
+
mockCreatePiSession.mockImplementationOnce(async () => {
|
|
543
|
+
return new FakeSession(async () => {
|
|
544
|
+
await fsp.mkdir(path.join(tmpDir, slug, 'chunks'), { recursive: true });
|
|
545
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'outline.json'), JSON.stringify({ slug, source: 'sources/', items }, null, 2));
|
|
546
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'index.json'), JSON.stringify({ slug, title: 'O', summary: '(refining)', tags: [], chunks: [] }, null, 2));
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
// S2 item0: 两次重试都只写 chunk-001.md,从不更新 index.json(add_chunk 未调用)
|
|
550
|
+
const writeOrphan = async () => {
|
|
551
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'chunks', 'chunk-001.md'), '# A');
|
|
552
|
+
};
|
|
553
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => { await writeOrphan(); })));
|
|
554
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => { await writeOrphan(); })));
|
|
555
|
+
// S3: 兜底成功(index.json chunks 为空也能跑,add_topic 只覆盖元数据)
|
|
556
|
+
mockCreatePiSession.mockImplementation(async () => {
|
|
557
|
+
return new FakeSession(FakeSession.addTopicHandler(['tag1']));
|
|
558
|
+
});
|
|
559
|
+
const events = makeEvents();
|
|
560
|
+
const result = await runLearnPipeline({
|
|
561
|
+
sourceType: 'file',
|
|
562
|
+
input: '# body',
|
|
563
|
+
label: `${slug}.md`,
|
|
564
|
+
port: 9999,
|
|
565
|
+
events,
|
|
566
|
+
docsDir: tmpDir,
|
|
567
|
+
});
|
|
568
|
+
expect(result.chunksWritten).toBe(0);
|
|
569
|
+
expect(result.itemsFailed).toBe(1);
|
|
570
|
+
const failed = events.calls.filter((c) => c.startsWith('stageItemFailed:'));
|
|
571
|
+
expect(failed).toHaveLength(1);
|
|
572
|
+
expect(failed[0]).toContain('add_chunk 未调用');
|
|
573
|
+
});
|
|
574
|
+
test('runPrompt 异常退出时在途轮次补 roundEnd:session 只 emit message_start 就 reject', async () => {
|
|
575
|
+
// 自造一个 session:prompt 期间只发 assistant message_start 随后抛错(模拟 abort)
|
|
576
|
+
const listeners = new Set();
|
|
577
|
+
const broken = {
|
|
578
|
+
sessionId: 'broken',
|
|
579
|
+
subscribe: (fn) => {
|
|
580
|
+
listeners.add(fn);
|
|
581
|
+
return () => { listeners.delete(fn); };
|
|
582
|
+
},
|
|
583
|
+
prompt: async () => {
|
|
584
|
+
for (const l of listeners)
|
|
585
|
+
l({ type: 'message_start', message: { role: 'assistant' } });
|
|
586
|
+
throw new Error('aborted mid-round');
|
|
587
|
+
},
|
|
588
|
+
waitForIdle: async () => { },
|
|
589
|
+
abort: async () => { },
|
|
590
|
+
};
|
|
591
|
+
mockCreatePiSession.mockImplementation(async () => broken);
|
|
592
|
+
const events = makeEvents();
|
|
593
|
+
await expect(runLearnPipeline({
|
|
594
|
+
sourceType: 'file',
|
|
595
|
+
input: '# body',
|
|
596
|
+
label: 'x.md',
|
|
597
|
+
port: 9999,
|
|
598
|
+
events,
|
|
599
|
+
docsDir: tmpDir,
|
|
600
|
+
})).rejects.toThrow();
|
|
601
|
+
// 轮次必须闭合:roundStart:0 有且仅有一次配对的 roundEnd:0
|
|
602
|
+
expect(events.calls).toContain('roundStart:0');
|
|
603
|
+
expect(events.calls.filter((c) => c === 'roundEnd:0')).toHaveLength(1);
|
|
604
|
+
});
|
|
605
|
+
// ============================================================
|
|
606
|
+
// url 模式:crawl 阶段由 LLM agent 驱动
|
|
607
|
+
// ============================================================
|
|
608
|
+
test('url happy path:crawl 收集入口+同站子页,round 数 = assistant 消息数', async () => {
|
|
609
|
+
crawlToolsMock.__setGraph({
|
|
610
|
+
'https://pi.dev/': { title: 'Entry', text: 'Entry text', links: ['https://pi.dev/child'] },
|
|
611
|
+
'https://pi.dev/child': { title: 'Child', text: 'Child text', links: [] },
|
|
612
|
+
});
|
|
613
|
+
const events = makeEvents();
|
|
614
|
+
const result = await runLearnPipeline({
|
|
615
|
+
sourceType: 'url',
|
|
616
|
+
input: 'https://pi.dev/',
|
|
617
|
+
port: 9999,
|
|
618
|
+
events,
|
|
619
|
+
docsDir: tmpDir,
|
|
620
|
+
});
|
|
621
|
+
// sources:入口 + 同站子页共 2 份
|
|
622
|
+
const files = await fsp.readdir(path.join(tmpDir, 'pi', 'sources'));
|
|
623
|
+
expect(files.filter((f) => f.endsWith('.md'))).toHaveLength(2);
|
|
624
|
+
expect(result.slug).toBe('pi');
|
|
625
|
+
expect(result.chunksWritten).toBe(1);
|
|
626
|
+
// round:crawl + outline + refine + tag = 4 轮,起止索引严格配对
|
|
627
|
+
expect(events.calls.filter((c) => c.startsWith('roundStart:'))).toEqual([
|
|
628
|
+
'roundStart:0', 'roundStart:1', 'roundStart:2', 'roundStart:3',
|
|
629
|
+
]);
|
|
630
|
+
expect(events.calls.filter((c) => c.startsWith('roundEnd:'))).toEqual([
|
|
631
|
+
'roundEnd:0', 'roundEnd:1', 'roundEnd:2', 'roundEnd:3',
|
|
632
|
+
]);
|
|
633
|
+
});
|
|
634
|
+
test('url 首轮 0 页 → 全新 guard+session 重试 1 次后成功', async () => {
|
|
635
|
+
crawlToolsMock.__setGraph({
|
|
636
|
+
'https://pi.dev/': { title: 'Entry', text: 'Entry text', links: ['https://pi.dev/child'] },
|
|
637
|
+
'https://pi.dev/child': { title: 'Child', text: 'Child text', links: [] },
|
|
638
|
+
});
|
|
639
|
+
// 第一个 crawl session:不保存任何页
|
|
640
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(async () => { }));
|
|
641
|
+
const events = makeEvents();
|
|
642
|
+
const result = await runLearnPipeline({
|
|
643
|
+
sourceType: 'url',
|
|
644
|
+
input: 'https://pi.dev/',
|
|
645
|
+
port: 9999,
|
|
646
|
+
events,
|
|
647
|
+
docsDir: tmpDir,
|
|
648
|
+
});
|
|
649
|
+
// 两个 crawl session(guard 也重建)+ outline/refine/tag
|
|
650
|
+
const crawlSessions = mockCreatePiSession.mock.calls.filter(([o]) => o.mode === 'crawl');
|
|
651
|
+
expect(crawlSessions).toHaveLength(2);
|
|
652
|
+
expect(crawlToolsMock.CrawlGuard).toHaveBeenCalledTimes(2);
|
|
653
|
+
await expect(fsp.readdir(path.join(tmpDir, 'pi', 'sources'))).resolves.toHaveLength(2);
|
|
654
|
+
expect(result.chunksWritten).toBe(1);
|
|
655
|
+
// 总轮次:2 crawl + outline + refine + tag = 5
|
|
656
|
+
expect(events.calls.filter((c) => c.startsWith('roundStart:'))).toHaveLength(5);
|
|
657
|
+
});
|
|
658
|
+
test('url 重试仍 0 页 → throw「crawl 未收集到页面」,guard 被关闭,stageEnd(outline) 发出', async () => {
|
|
659
|
+
crawlToolsMock.__setGraph({
|
|
660
|
+
'https://pi.dev/': { title: 'Entry', text: 'Entry text', links: [] },
|
|
661
|
+
});
|
|
662
|
+
// 两个 crawl session 均不保存页
|
|
663
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(async () => { }));
|
|
664
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(async () => { }));
|
|
665
|
+
const events = makeEvents();
|
|
666
|
+
await expect(runLearnPipeline({
|
|
667
|
+
sourceType: 'url',
|
|
668
|
+
input: 'https://pi.dev/',
|
|
669
|
+
port: 9999,
|
|
670
|
+
events,
|
|
671
|
+
docsDir: tmpDir,
|
|
672
|
+
})).rejects.toThrow(/crawl 未收集到页面/);
|
|
673
|
+
// 建了 2 个 guard 且都被 close(finally 关闭当前 guard;首个 guard 在替换时关闭)
|
|
674
|
+
expect(crawlToolsMock.CrawlGuard).toHaveBeenCalledTimes(2);
|
|
675
|
+
const guards = crawlToolsMock.CrawlGuard.mock.instances;
|
|
676
|
+
expect(guards.every((g) => g.closed)).toBe(true);
|
|
677
|
+
expect(events.calls).toContain('stageEnd:outline');
|
|
678
|
+
});
|
|
679
|
+
// ============================================================
|
|
680
|
+
// 查重门接入(门1 outline 后 / 门2 tag 后)
|
|
681
|
+
// ============================================================
|
|
682
|
+
test('门1 match:outline 后命中 oldt → 物理并入 + 续号 chunk-002 + 对 oldt 跑 tag', async () => {
|
|
683
|
+
await seedOldTopic('oldt');
|
|
684
|
+
const slug = 'new-topic';
|
|
685
|
+
// 门1 裁定命中:只新增条目 n
|
|
686
|
+
mockReconcile.mockResolvedValueOnce({
|
|
687
|
+
match: true,
|
|
688
|
+
targetSlug: 'oldt',
|
|
689
|
+
mergedTitle: 'M',
|
|
690
|
+
mergedSummary: 's',
|
|
691
|
+
itemsToAdd: [{ id: 'n', title: 'N', oneLine: 'n' }],
|
|
692
|
+
});
|
|
693
|
+
// S1: 门1 专用 seed(chunks: [])
|
|
694
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(gate1OutlineHandler(slug, [{ id: 'a', title: '概念A', oneLine: 'aaa' }])));
|
|
695
|
+
// S2(merge 后对 itemsToAdd 跑,startOffset=1):写 chunk-002.md + 更新 dst index 注册
|
|
696
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => {
|
|
697
|
+
await fsp.writeFile(path.join(tmpDir, 'oldt', 'chunks', 'chunk-002.md'), '# N');
|
|
698
|
+
const idxPath = path.join(tmpDir, 'oldt', 'index.json');
|
|
699
|
+
const idx = JSON.parse(await fsp.readFile(idxPath, 'utf8'));
|
|
700
|
+
idx.chunks.push({ id: 'chunk-002', title: 'N', summary: 'n', tags: [] });
|
|
701
|
+
await fsp.writeFile(idxPath, JSON.stringify(idx, null, 2));
|
|
702
|
+
})));
|
|
703
|
+
// S3: 对 oldt 跑 tag,捕获 prompt
|
|
704
|
+
const tagPrompts = [];
|
|
705
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(tagCaptureHandler(tagPrompts)));
|
|
706
|
+
const result = await runLearnPipeline({
|
|
707
|
+
sourceType: 'file',
|
|
708
|
+
input: '# body',
|
|
709
|
+
label: `${slug}.md`,
|
|
710
|
+
port: 9999,
|
|
711
|
+
events: makeEvents(),
|
|
712
|
+
docsDir: tmpDir,
|
|
713
|
+
});
|
|
714
|
+
// 返回目标 slug;refine 只写了 1 个新增 chunk
|
|
715
|
+
expect(result.slug).toBe('oldt');
|
|
716
|
+
expect(result.chunksWritten).toBe(1);
|
|
717
|
+
expect(result.itemsFailed).toBe(0);
|
|
718
|
+
// oldt 原有 chunk-001 + 续号 chunk-002;src 目录已消失
|
|
719
|
+
const chunks = await fsp.readdir(path.join(tmpDir, 'oldt', 'chunks'));
|
|
720
|
+
expect(chunks).toEqual(expect.arrayContaining(['chunk-001.md', 'chunk-002.md']));
|
|
721
|
+
await expect(fsp.access(path.join(tmpDir, slug))).rejects.toThrow();
|
|
722
|
+
// 根 index 未新增主题,仍只有 oldt
|
|
723
|
+
const root = JSON.parse(await fsp.readFile(path.join(tmpDir, 'index.json'), 'utf8'));
|
|
724
|
+
expect(root.topics).toHaveLength(1);
|
|
725
|
+
expect(root.topics[0].slug).toBe('oldt');
|
|
726
|
+
// tag 对 oldt 执行,且 prompt 使用合并后的 override title/summary
|
|
727
|
+
expect(tagPrompts).toHaveLength(1);
|
|
728
|
+
expect(tagPrompts[0]).toContain('主题 slug: oldt');
|
|
729
|
+
expect(tagPrompts[0]).toContain('M');
|
|
730
|
+
expect(tagPrompts[0]).toContain('s');
|
|
731
|
+
});
|
|
732
|
+
test('门2 match:常规 S2/S3 后命中 oldt → src 消失、oldt 出现重编号 chunk、再对 oldt 跑 tag', async () => {
|
|
733
|
+
await seedOldTopic('oldt');
|
|
734
|
+
const slug = 'new-topic2';
|
|
735
|
+
const items = [{ id: 'a', title: '概念A', oneLine: 'aaa' }];
|
|
736
|
+
// reconcile 调用次序:门1 no-match → 门2 match
|
|
737
|
+
mockReconcile.mockResolvedValueOnce({ match: false });
|
|
738
|
+
mockReconcile.mockResolvedValueOnce({
|
|
739
|
+
match: true,
|
|
740
|
+
targetSlug: 'oldt',
|
|
741
|
+
mergedTitle: 'M2',
|
|
742
|
+
mergedSummary: 's2',
|
|
743
|
+
});
|
|
744
|
+
// S1: 常规 seed(chunks 带 chunk-001 ref)+ chunk 正文
|
|
745
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(async () => {
|
|
746
|
+
await fsp.mkdir(path.join(tmpDir, slug, 'chunks'), { recursive: true });
|
|
747
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'outline.json'), JSON.stringify({ slug, source: 'sources/', items }, null, 2));
|
|
748
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'index.json'), JSON.stringify({ slug, title: 'New2', summary: '(refining)', tags: [], chunks: [
|
|
749
|
+
{ id: 'chunk-001', title: '概念A', summary: 'aaa', tags: [] },
|
|
750
|
+
] }, null, 2));
|
|
751
|
+
await fsp.writeFile(path.join(tmpDir, slug, 'chunks', 'chunk-001.md'), '# 概念A');
|
|
752
|
+
}));
|
|
753
|
+
// S2 item0: chunk-001 已存在且已注册,仅 read_source 成功
|
|
754
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => { })));
|
|
755
|
+
// S3 第一次 tag:对 src slug
|
|
756
|
+
// S3 第二次 tag(门2 merge 后):对 oldt,使用 override
|
|
757
|
+
const tagPrompts = [];
|
|
758
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(tagCaptureHandler(tagPrompts)));
|
|
759
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(tagCaptureHandler(tagPrompts)));
|
|
760
|
+
const result = await runLearnPipeline({
|
|
761
|
+
sourceType: 'file',
|
|
762
|
+
input: '# body',
|
|
763
|
+
label: `${slug}.md`,
|
|
764
|
+
port: 9999,
|
|
765
|
+
events: makeEvents(),
|
|
766
|
+
docsDir: tmpDir,
|
|
767
|
+
});
|
|
768
|
+
expect(result.slug).toBe('oldt');
|
|
769
|
+
expect(result.chunksWritten).toBe(1);
|
|
770
|
+
// src 目录消失;oldt 拿到重编号的迁移 chunk-002
|
|
771
|
+
await expect(fsp.access(path.join(tmpDir, slug))).rejects.toThrow();
|
|
772
|
+
const chunks = await fsp.readdir(path.join(tmpDir, 'oldt', 'chunks'));
|
|
773
|
+
expect(chunks).toEqual(expect.arrayContaining(['chunk-001.md', 'chunk-002.md']));
|
|
774
|
+
// tag 跑了两次:先 src,后 oldt(override M2/s2)
|
|
775
|
+
expect(tagPrompts).toHaveLength(2);
|
|
776
|
+
expect(tagPrompts[0]).toContain(`主题 slug: ${slug}`);
|
|
777
|
+
expect(tagPrompts[1]).toContain('主题 slug: oldt');
|
|
778
|
+
expect(tagPrompts[1]).toContain('M2');
|
|
779
|
+
expect(tagPrompts[1]).toContain('s2');
|
|
780
|
+
});
|
|
781
|
+
// ============================================================
|
|
782
|
+
// 门0:slug 精确命中(旁挂 / 还原)
|
|
783
|
+
// ============================================================
|
|
784
|
+
test('门0 match:同 slug 命中 → 旧 chunks 续号并入 + 新增 chunk-003,只保留 dup 目录', async () => {
|
|
785
|
+
await seedDupTopic();
|
|
786
|
+
// 门0 裁定命中:只新增条目 n(门1/门2 不再到达)
|
|
787
|
+
mockReconcile.mockResolvedValueOnce({
|
|
788
|
+
match: true,
|
|
789
|
+
targetSlug: 'dup',
|
|
790
|
+
mergedTitle: 'M',
|
|
791
|
+
mergedSummary: 's',
|
|
792
|
+
itemsToAdd: [{ id: 'n', title: 'N', oneLine: 'n' }],
|
|
793
|
+
});
|
|
794
|
+
// S1 outline session: 旁挂后 fresh dup 上写 outline + seed(chunks: [])
|
|
795
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(gate0OutlineHandler('dup', [{ id: 'a', title: '概念A', oneLine: 'aaa' }])));
|
|
796
|
+
// S2(merge 后 startOffset=2):写 chunk-003.md + 在 dst index 注册
|
|
797
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => {
|
|
798
|
+
await fsp.writeFile(path.join(tmpDir, 'dup', 'chunks', 'chunk-003.md'), '# N');
|
|
799
|
+
const idxPath = path.join(tmpDir, 'dup', 'index.json');
|
|
800
|
+
const idx = JSON.parse(await fsp.readFile(idxPath, 'utf8'));
|
|
801
|
+
idx.chunks.push({ id: 'chunk-003', title: 'N', summary: 'n', tags: [] });
|
|
802
|
+
await fsp.writeFile(idxPath, JSON.stringify(idx, null, 2));
|
|
803
|
+
})));
|
|
804
|
+
// S3: 对 dup 带 merged override 跑 tag
|
|
805
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.addTopicHandler(['merged-tag'])));
|
|
806
|
+
const result = await runLearnPipeline({
|
|
807
|
+
sourceType: 'file',
|
|
808
|
+
input: '# new body',
|
|
809
|
+
label: 'dup.md',
|
|
810
|
+
port: 9999,
|
|
811
|
+
events: makeEvents(),
|
|
812
|
+
docsDir: tmpDir,
|
|
813
|
+
});
|
|
814
|
+
expect(result.slug).toBe('dup');
|
|
815
|
+
expect(result.chunksWritten).toBe(1);
|
|
816
|
+
expect(result.itemsFailed).toBe(0);
|
|
817
|
+
// 只剩 dup 主题目录(无 dup.old)
|
|
818
|
+
const dirs = (await fsp.readdir(tmpDir, { withFileTypes: true }))
|
|
819
|
+
.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
820
|
+
expect(dirs).toEqual(['dup']);
|
|
821
|
+
// 旧 2 chunks + 续号 003;旧正文逐字不变
|
|
822
|
+
const chunks = await fsp.readdir(path.join(tmpDir, 'dup', 'chunks'));
|
|
823
|
+
expect(chunks).toEqual(expect.arrayContaining(['chunk-001.md', 'chunk-002.md', 'chunk-003.md']));
|
|
824
|
+
expect(await fsp.readFile(path.join(tmpDir, 'dup', 'chunks', 'chunk-001.md'), 'utf8')).toBe('OLD BODY 001\n');
|
|
825
|
+
expect(await fsp.readFile(path.join(tmpDir, 'dup', 'chunks', 'chunk-002.md'), 'utf8')).toBe('OLD BODY 002\n');
|
|
826
|
+
});
|
|
827
|
+
test('门0 no-match:slug 碰撞 → 新内容另铸 dup-new 继续,旧 dup 原样还原', async () => {
|
|
828
|
+
await seedDupTopic();
|
|
829
|
+
mockMintSlug.mockResolvedValue('dup-new');
|
|
830
|
+
// 门0/门1/门2 全部 no-match(默认值已满足,无需额外配置)
|
|
831
|
+
// S1 outline session
|
|
832
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(gate0OutlineHandler('dup', [{ id: 'a', title: '概念A', oneLine: 'aaa' }])));
|
|
833
|
+
// S2:对 activeSlug=dup-new 写 chunk-001 + 注册
|
|
834
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.readSourceThen(async () => {
|
|
835
|
+
await fsp.mkdir(path.join(tmpDir, 'dup-new', 'chunks'), { recursive: true });
|
|
836
|
+
await fsp.writeFile(path.join(tmpDir, 'dup-new', 'chunks', 'chunk-001.md'), '# NEW');
|
|
837
|
+
const idxPath = path.join(tmpDir, 'dup-new', 'index.json');
|
|
838
|
+
const idx = JSON.parse(await fsp.readFile(idxPath, 'utf8'));
|
|
839
|
+
idx.chunks.push({ id: 'chunk-001', title: '概念A', summary: 'aaa', tags: [] });
|
|
840
|
+
await fsp.writeFile(idxPath, JSON.stringify(idx, null, 2));
|
|
841
|
+
})));
|
|
842
|
+
// S3: 对 dup-new 常规 tag
|
|
843
|
+
mockCreatePiSession.mockImplementationOnce(async () => new FakeSession(FakeSession.addTopicHandler(['new-tag'])));
|
|
844
|
+
const result = await runLearnPipeline({
|
|
845
|
+
sourceType: 'file',
|
|
846
|
+
input: '# new body',
|
|
847
|
+
label: 'dup.md',
|
|
848
|
+
port: 9999,
|
|
849
|
+
events: makeEvents(),
|
|
850
|
+
docsDir: tmpDir,
|
|
851
|
+
});
|
|
852
|
+
expect(result.slug).toBe('dup-new');
|
|
853
|
+
expect(result.chunksWritten).toBe(1);
|
|
854
|
+
// 无 .old;dup 与 dup-new 两个主题目录共存
|
|
855
|
+
const dirs = (await fsp.readdir(tmpDir, { withFileTypes: true }))
|
|
856
|
+
.filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
857
|
+
expect(dirs).toEqual(['dup', 'dup-new']);
|
|
858
|
+
// 旧 dup:2 个原始 chunks 正文原样,sources 原样
|
|
859
|
+
expect(await fsp.readdir(path.join(tmpDir, 'dup', 'chunks')))
|
|
860
|
+
.toEqual(expect.arrayContaining(['chunk-001.md', 'chunk-002.md']));
|
|
861
|
+
expect(await fsp.readFile(path.join(tmpDir, 'dup', 'chunks', 'chunk-001.md'), 'utf8')).toBe('OLD BODY 001\n');
|
|
862
|
+
expect(await fsp.readFile(path.join(tmpDir, 'dup', 'chunks', 'chunk-002.md'), 'utf8')).toBe('OLD BODY 002\n');
|
|
863
|
+
expect(await fsp.readFile(path.join(tmpDir, 'dup', 'sources', 'source-001.md'), 'utf8')).toBe('OLD SOURCE\n');
|
|
864
|
+
// 新 dup-new:自有 sources + 新 chunk
|
|
865
|
+
expect(await fsp.readdir(path.join(tmpDir, 'dup-new', 'sources'))).toEqual(['source-001.md']);
|
|
866
|
+
expect(await fsp.readFile(path.join(tmpDir, 'dup-new', 'chunks', 'chunk-001.md'), 'utf8')).toBe('# NEW');
|
|
867
|
+
// 根 index:dup 与 dup-new 均登记
|
|
868
|
+
const root = JSON.parse(await fsp.readFile(path.join(tmpDir, 'index.json'), 'utf8'));
|
|
869
|
+
expect(root.topics.map((t) => t.slug).sort()).toEqual(['dup', 'dup-new']);
|
|
870
|
+
});
|
|
871
|
+
test('阻断:assertNoOldDirs 抛错 → pipeline reject 且不创建任何 session', async () => {
|
|
872
|
+
mockAssertNoOld.mockImplementation(() => {
|
|
873
|
+
throw new Error('检测到未清理的旁挂目录 x.old,请手工处理后再学习');
|
|
874
|
+
});
|
|
875
|
+
await expect(runLearnPipeline({
|
|
876
|
+
sourceType: 'file',
|
|
877
|
+
input: '# body',
|
|
878
|
+
label: 'any.md',
|
|
879
|
+
port: 9999,
|
|
880
|
+
events: makeEvents(),
|
|
881
|
+
docsDir: tmpDir,
|
|
882
|
+
})).rejects.toThrow(/检测到未清理的旁挂目录/);
|
|
883
|
+
expect(mockCreatePiSession).not.toHaveBeenCalled();
|
|
884
|
+
});
|
|
885
|
+
});
|