wednesdayai 0.4.12-dev.715 → 0.4.12-dev.717
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/{agent-turn-enqueue-Dwz_R_DW.js → agent-turn-enqueue-Cqo34Swm.js} +4 -4
- package/dist/{agent-turn-enqueue-Da7B-J9y.js → agent-turn-enqueue-N-q0v3mc.js} +4 -4
- package/dist/{agent-turn-execute-DvP2nf2J.js → agent-turn-execute-Bw9hhkAP.js} +69 -24
- package/dist/{agent-turn-recovery-adapter-SdyNUgzD.js → agent-turn-recovery-adapter-BkIal4FW.js} +1 -1
- package/dist/{agent-turn-recovery-adapter-DF3vdyE5.js → agent-turn-recovery-adapter-CLY2Dn04.js} +1 -1
- package/dist/{agent-turn-runner-D20dK54z.js → agent-turn-runner-BsdyAURQ.js} +1 -1
- package/dist/{agent-turn-runner-CzcuyJfX.js → agent-turn-runner-CImqp_RN.js} +1 -1
- package/dist/{agent-turn-runner-PbFFnq5P.js → agent-turn-runner-CUCOdxhH.js} +1 -1
- package/dist/{agent-turn-runner-DFZxbjaC.js → agent-turn-runner-Vd3aTCni.js} +1 -1
- package/dist/{agents-D7wjsCEZ.js → agents-BBoIqOw6.js} +4 -4
- package/dist/{agents.config-iUjxdoPw.js → agents.config-C8RJrfbr.js} +1 -1
- package/dist/{agents.config-C-pTu-0T.js → agents.config-Ctnm8o8d.js} +1 -1
- package/dist/{audio-preflight-nrbQcAOS.js → audio-preflight-BKQpd1eU.js} +6 -6
- package/dist/{audio-preflight-BZFphz4E.js → audio-preflight-bEi-KkBn.js} +6 -6
- package/dist/{audio-transcription-runner-C2Q9mEhB.js → audio-transcription-runner-BD7UgbNj.js} +1 -1
- package/dist/{audio-transcription-runner-D-d42kdt.js → audio-transcription-runner-CGTNNwbm.js} +1 -1
- package/dist/{audit-BlUclRcb.js → audit-BM8WZyx6.js} +3 -3
- package/dist/{audit-C_g-UVNn.js → audit-CcNEpaG6.js} +3 -3
- package/dist/{auth-choice-ZsDU-57A.js → auth-choice-DTg-b2da.js} +1 -1
- package/dist/{auth-choice-D9ydQCkO.js → auth-choice-VnJYar-i.js} +1 -1
- package/dist/{banner-U4ziIXHD.js → banner-_g-nCJoZ.js} +1 -1
- package/dist/build-info.json +3 -3
- package/dist/bundled/boot-md/handler.js +2 -2
- package/dist/bundled/session-journal/handler.js +2 -2
- package/dist/bundled/session-memory/handler.js +2 -2
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/{channel-options-IkZTBShY.js → channel-options-BX8MFfPW.js} +1 -1
- package/dist/{channel-options-CdXOmeJN.js → channel-options-Dod3uWTl.js} +1 -1
- package/dist/{channels-cli-CUTJf_vC.js → channels-cli-DyfFtAZa.js} +23 -23
- package/dist/{channels-cli-CBBGSlVe.js → channels-cli-MbAArXF1.js} +23 -23
- package/dist/cli/daemon-cli.js +1 -1
- package/dist/{cli-DCyFadMx.js → cli-CdrO5h4P.js} +17 -17
- package/dist/{cli-BtY_juql.js → cli-DF91S8Cf.js} +17 -17
- package/dist/{command-queue-B1RxJ6bM.js → command-queue-DmXWCoY0.js} +1 -1
- package/dist/{command-queue-6Jucg1io.js → command-queue-URs9pYSC.js} +1 -1
- package/dist/{command-registry-BMq2CUSD.js → command-registry-C7nkhshN.js} +9 -9
- package/dist/{completion-cli-CATJLvVa.js → completion-cli-CjPs9az4.js} +2 -2
- package/dist/{completion-cli-Dbszfn8D.js → completion-cli-DozvikmZ.js} +1 -1
- package/dist/{config-cli-3a-9iNyI.js → config-cli-Dagtz6FG.js} +1 -1
- package/dist/{config-cli-O9P0EpgL.js → config-cli-Dv1Dad57.js} +1 -1
- package/dist/config.js +9 -9
- package/dist/{configure-DAA_ncD3.js → configure-BbKf530X.js} +5 -5
- package/dist/{configure-Dld8IaT8.js → configure-CBJSHejz.js} +5 -5
- package/dist/{daemon-cli-B3Q460ai.js → daemon-cli-CiPY543O.js} +5 -5
- package/dist/{daemon-cli-C5cAb15Z.js → daemon-cli-YmPcKhvo.js} +5 -5
- package/dist/daemon-cli.js +44 -3
- package/dist/{daemon-install-DGU1qIIK.js → daemon-install-BuETscSF.js} +3 -3
- package/dist/{daemon-install-CBIxgo4p.js → daemon-install-CGg5r1wy.js} +3 -3
- package/dist/{deliver-Bhlx9VG9.js → deliver-8xg5ISEI.js} +1 -1
- package/dist/{deliver-DT4v-Go0.js → deliver-C5SDo7TP.js} +1 -1
- package/dist/{diagnostic-BTSirPP0.js → diagnostic-D7DvV7NT.js} +1 -1
- package/dist/{diagnostic-DRvqMPW7.js → diagnostic-Dv8UWiI0.js} +1 -1
- package/dist/{diagnostics-C4-TUVnm.js → diagnostics-C6ntZHc3.js} +1 -1
- package/dist/{diagnostics-FIcvxigK.js → diagnostics-Ds3p_NPY.js} +1 -1
- package/dist/{dm-policy-shared-BE04dZam.js → dm-policy-shared-BLJbsZzf.js} +1 -1
- package/dist/{doctor-completion-BJpzpRx6.js → doctor-completion-CJYV2FQ5.js} +1 -1
- package/dist/{doctor-completion-Ca_SREXp.js → doctor-completion-D4T2GzR7.js} +1 -1
- package/dist/entry.js +2 -2
- package/dist/{exec-approvals-cli-t7rAulRf.js → exec-approvals-cli-BAJ3sxzP.js} +3 -3
- package/dist/{exec-approvals-cli-DsXabPF_.js → exec-approvals-cli-DIUYxvM0.js} +3 -3
- package/dist/extensionAPI.js +2 -2
- package/dist/{gateway-cli-BTuK5wC4.js → gateway-cli-Cyivh-5T.js} +121 -84
- package/dist/{gateway-cli-BYqWX_fq.js → gateway-cli-DtFDQj5d.js} +121 -84
- package/dist/{health-fB9rLTPD.js → health-Cooqit6V.js} +2 -2
- package/dist/{health-DFWpyp9O.js → health-DFat5vO5.js} +2 -2
- package/dist/{hooks-cli-DKZeKlmX.js → hooks-cli-CB-96Su8.js} +19 -19
- package/dist/{hooks-cli-CriP6g_q.js → hooks-cli-yNAgxRQS.js} +19 -19
- package/dist/index.js +21 -21
- package/dist/{inspect-DVHAPbcZ.js → inspect-BXlZnWkg.js} +1 -1
- package/dist/{inspect-gvjq38z1.js → inspect-LfUprDjx.js} +1 -1
- package/dist/{installs-CBHXvqKf.js → installs-CdLdL1gD.js} +1 -1
- package/dist/{installs-CnAcm0ba.js → installs-CpnJwZZ_.js} +1 -1
- package/dist/{lifecycle-core-BdCcww82.js → lifecycle-core-CuMY0U_8.js} +3 -3
- package/dist/{lifecycle-core-B3-ezmTx.js → lifecycle-core-M-5Z8Xml.js} +3 -3
- package/dist/llm-slug-generator.js +2 -2
- package/dist/{models-DXRXVywP.js → models-Dlat36jl.js} +1 -1
- package/dist/{models-cli-C7bSvLGL.js → models-cli-DMjHkAqq.js} +17 -17
- package/dist/{models-cli-h6ejcNF7.js → models-cli-vjFaOcNS.js} +18 -18
- package/dist/{node-cli-CS8E71Vc.js → node-cli-Csimlxec.js} +8 -8
- package/dist/{node-cli-CPo0jR3t.js → node-cli-dh4qWKpT.js} +8 -8
- package/dist/{node-service-BnwLXY5x.js → node-service-BnyKZNqa.js} +1 -1
- package/dist/{node-service-r64yexlb.js → node-service-Ys7JZFz-.js} +1 -1
- package/dist/{npm-pack-install-CFiN84Qw.js → npm-pack-install-B7wQANiM.js} +1 -1
- package/dist/{npm-pack-install-bdyXL1bZ.js → npm-pack-install-DcBobJp3.js} +1 -1
- package/dist/{npm-resolution-CaEc_FBL.js → npm-resolution-B0rqOBgy.js} +2 -2
- package/dist/{npm-resolution-B7regLLc.js → npm-resolution-SdoNL0ud.js} +2 -2
- package/dist/{onboard-B1WwaAdf.js → onboard-DuNuHUvh.js} +3 -3
- package/dist/{onboard-B_6H9HpS.js → onboard-_pVU_O8s.js} +3 -3
- package/dist/{onboard-channels-BWrbGzK9.js → onboard-channels-28JqVvAv.js} +2 -2
- package/dist/{onboard-channels-CxiuviBR.js → onboard-channels-P7F-dNwm.js} +2 -2
- package/dist/{onboarding-DemzhSFH.js → onboarding-BBb8ZbfX.js} +3 -3
- package/dist/{onboarding-D9VO9_5U.js → onboarding-BcI8N4WK.js} +3 -3
- package/dist/{onboarding.finalize-C8Pqseaq.js → onboarding.finalize-BXkz7oPa.js} +26 -26
- package/dist/{onboarding.finalize-DeJ35Prz.js → onboarding.finalize-D9fyrhAS.js} +25 -25
- package/dist/{operator-commands-Dwtv8SyF.js → operator-commands-CIz47IKl.js} +2 -2
- package/dist/{operator-commands-BRKxpjt9.js → operator-commands-CtYCnspr.js} +2 -2
- package/dist/{pi-embedded-BcRvgwKi.js → pi-embedded-Qyba-Rws.js} +22 -1
- package/dist/{pi-embedded-helpers-D19zTumt.js → pi-embedded-helpers-BU47jdPO.js} +1 -1
- package/dist/{pi-embedded-helpers-DEHXX7Dr.js → pi-embedded-helpers-i0-tPaxH.js} +1 -1
- package/dist/{pi-embedded-recovery-adapter-MaU4SQ3X.js → pi-embedded-recovery-adapter-1Lgt8q1h.js} +2 -2
- package/dist/{pi-embedded-recovery-adapter-licel-RD.js → pi-embedded-recovery-adapter-B2AFWrYt.js} +2 -2
- package/dist/{pi-embedded-recovery-adapter-ZV9u5snz.js → pi-embedded-recovery-adapter-CNdSU-s7.js} +8 -8
- package/dist/{pi-embedded-recovery-adapter-DB4blfKA.js → pi-embedded-recovery-adapter-DL78oCTf.js} +8 -8
- package/dist/{pi-embedded-utils-oIHgFob3.js → pi-embedded-utils-8OzPnJMh.js} +1 -1
- package/dist/{pi-embedded-utils-BhOMYPQU.js → pi-embedded-utils-BtOzugyp.js} +1 -1
- package/dist/{pi-embedded-DsDG-ETP.js → pi-embedded-wjGM_bNN.js} +22 -1
- package/dist/{pi-tools.policy-4GqIom7_.js → pi-tools.policy-DPm2eos4.js} +1 -1
- package/dist/{plugin-registry-CxDNClOG.js → plugin-registry-CC8YsXPr.js} +1 -1
- package/dist/{plugin-registry-alYBvHFE.js → plugin-registry-CPS5bO_-.js} +1 -1
- package/dist/plugin-sdk/{agent-scope-BLF4nZUv.js → agent-scope-BYp_CjdU.js} +2 -2
- package/dist/plugin-sdk/{agent-scope-BQYVQxfQ.js → agent-scope-DU8uJiiv.js} +2 -2
- package/dist/plugin-sdk/agent-turn-execute-BCE3FS2s.js +61 -0
- package/dist/plugin-sdk/agent-turn-execute-DIrSWfHZ.js +61 -0
- package/dist/plugin-sdk/{agent-turn-runner-YBWX_qXk.js → agent-turn-runner-B9_FKVSu.js} +6 -6
- package/dist/plugin-sdk/{agent-turn-runner-CDA8JPSH.js → agent-turn-runner-CmDXH4Z8.js} +6 -6
- package/dist/plugin-sdk/agent-turn-runner-gf0ARmuP.js +11 -0
- package/dist/plugin-sdk/{api-key-rotation-XDUKz2gd.js → api-key-rotation-CDNf_4cI.js} +2 -2
- package/dist/plugin-sdk/{api-key-rotation-BW3rq9Tg.js → api-key-rotation-Oe0hkmVQ.js} +2 -2
- package/dist/plugin-sdk/{artifacts-HAYpGlG1.js → artifacts-BMLMg3MG.js} +1 -1
- package/dist/plugin-sdk/{artifacts-C8xK4Y3e.js → artifacts-Dk-7TtTn.js} +1 -1
- package/dist/plugin-sdk/{audio-preflight-Bi8RI9zM.js → audio-preflight-8hRbl8Io.js} +22 -22
- package/dist/plugin-sdk/{audio-preflight-BS29OWk3.js → audio-preflight-BmKL6FNP.js} +2 -2
- package/dist/plugin-sdk/audio-preflight-DlGohKMs.js +28 -0
- package/dist/plugin-sdk/{audio-transcription-runner-CGgNZVZv.js → audio-transcription-runner-CUsZFiEA.js} +11 -11
- package/dist/plugin-sdk/{audio-transcription-runner-BNafitZj.js → audio-transcription-runner-TBjJv_mZ.js} +10 -10
- package/dist/plugin-sdk/{auth-profiles-CCJQoedi.js → auth-profiles-DHZjW8Vk.js} +5 -5
- package/dist/plugin-sdk/{auth-profiles-CIKTWees.js → auth-profiles-OgKl-vr-.js} +5 -5
- package/dist/plugin-sdk/{channel-activity-BKwRzYJb.js → channel-activity-Dr4PYej7.js} +3 -3
- package/dist/plugin-sdk/{channel-activity-Ck83U_es.js → channel-activity-OIauZrkN.js} +3 -3
- package/dist/plugin-sdk/{chrome-D0eLSqkP.js → chrome-BDy1zYAr.js} +4 -4
- package/dist/plugin-sdk/{chrome-CT_6RvJg.js → chrome-VZ0Zuu2w.js} +4 -4
- package/dist/plugin-sdk/{chunk-DI-5pBck.js → chunk-BTSnO1wn.js} +6 -6
- package/dist/plugin-sdk/{chunk-D6yDhUpF.js → chunk-BWz-lIzS.js} +4 -4
- package/dist/plugin-sdk/{chunk-CDvvv9SU.js → chunk-DGQdf0JN.js} +3 -3
- package/dist/plugin-sdk/{command-detection-B3xgM_Na.js → command-detection-DdTnFMU3.js} +106 -85
- package/dist/plugin-sdk/{command-queue-BnJsJc_w.js → command-queue-CeaLVs8O.js} +8 -8
- package/dist/plugin-sdk/{command-queue-CHiQj9Ji.js → command-queue-DFSOdqCK.js} +1 -1
- package/dist/plugin-sdk/{command-queue-DH8k55DC.js → command-queue-D_UFlrwC.js} +2 -2
- package/dist/plugin-sdk/{commands-registry-Do_g2It_.js → commands-registry-8sy0wsK_.js} +6 -6
- package/dist/plugin-sdk/{commands-registry-DpLk11E7.js → commands-registry-N6f_lsxF.js} +2 -2
- package/dist/plugin-sdk/{commands-registry-DuwlD_aI.js → commands-registry-XW2hmbAE.js} +3 -3
- package/dist/plugin-sdk/{config-CQNfmIC8.js → config-Biusjzx-.js} +7 -7
- package/dist/plugin-sdk/{deliver-_UyigkEK.js → deliver-B7W4NNsR.js} +11 -11
- package/dist/plugin-sdk/{deliver-L6eu6zyG.js → deliver-BgHA8c9F.js} +11 -11
- package/dist/plugin-sdk/deliver-CJP9TznW.js +32 -0
- package/dist/plugin-sdk/deliver-DBCz1GqU.js +32 -0
- package/dist/plugin-sdk/{diagnostic-Dk57KVjE.js → diagnostic-BRUM6uNI.js} +7 -7
- package/dist/plugin-sdk/{diagnostic-DfxuMWHd.js → diagnostic-Bni_8VyY.js} +7 -7
- package/dist/plugin-sdk/{diagnostic-C7lPoqyv.js → diagnostic-D6hSjxiv.js} +5 -5
- package/dist/plugin-sdk/{diagnostic-session-state-BjuWxrAM.js → diagnostic-session-state-BsHrQ7U3.js} +1 -1
- package/dist/plugin-sdk/{fetch-D1jXg-ju.js → fetch-C1vgNaXJ.js} +1 -1
- package/dist/plugin-sdk/{fetch-guard-DnwfMurj.js → fetch-guard-B2Wnl9Vi.js} +4 -4
- package/dist/plugin-sdk/{fetch-guard-CZT8FU1i.js → fetch-guard-C1a1KfEb.js} +3 -3
- package/dist/plugin-sdk/{fetch-DFZXuu3W.js → fetch-hEreHolP.js} +7 -7
- package/dist/plugin-sdk/{fetch-timeout-D7XVWfxj.js → fetch-timeout-A1m6BOhm.js} +1 -1
- package/dist/plugin-sdk/{fs-safe-B0JgGdXa.js → fs-safe-CbZ6uRVv.js} +3 -3
- package/dist/plugin-sdk/{fs-safe-BQ5cHy9G.js → fs-safe-ClebEeLz.js} +3 -3
- package/dist/plugin-sdk/{hook-runner-global-WRI53ibi.js → hook-runner-global-BvxlnAyu.js} +3 -3
- package/dist/plugin-sdk/{image-ops-0TjWNKUx.js → image-ops-Bm03YsK2.js} +1 -1
- package/dist/plugin-sdk/{image-ops-kuILYHZv.js → image-ops-DY_SZfXk.js} +1 -1
- package/dist/plugin-sdk/{inbound-context-Bfr5DkdO.js → inbound-context-Cmgtf5my.js} +1 -1
- package/dist/plugin-sdk/index.js +54 -54
- package/dist/plugin-sdk/{ir-CbL0sVL2.js → ir-1CthVxHv.js} +5 -5
- package/dist/plugin-sdk/{ir-Bm7oqkwT.js → ir-BNohi6ZW.js} +5 -5
- package/dist/plugin-sdk/{local-roots-D-CHzVr1.js → local-roots-Bm7nDW20.js} +7 -7
- package/dist/plugin-sdk/{logger-D5s3kiib.js → logger-BkzoyfHW.js} +1 -1
- package/dist/plugin-sdk/{logger-BSK-0Yjx.js → logger-D6KsqbNW.js} +1 -1
- package/dist/plugin-sdk/{manager-D2jffdjH.js → manager-B_vK9MbM.js} +8 -8
- package/dist/plugin-sdk/manager-ByaW_O4Y.js +16 -0
- package/dist/plugin-sdk/{manager-CHBt3PC-.js → manager-C4CJpRd9.js} +8 -8
- package/dist/plugin-sdk/manager-CPcI0Fhz.js +16 -0
- package/dist/plugin-sdk/{markdown-tables-CBGsimjo.js → markdown-tables-DZM4NfLU.js} +3 -3
- package/dist/plugin-sdk/{markdown-tables-DIWQQ2W1.js → markdown-tables-G4vK24xc.js} +2 -2
- package/dist/plugin-sdk/markdown-tables-J4hZ7eWU.js +10 -0
- package/dist/plugin-sdk/{outbound-attachment-CtKKNyms.js → outbound-attachment-Cw7T2Mrt.js} +2 -2
- package/dist/plugin-sdk/{outbound-attachment-C5x2htXW.js → outbound-attachment-GMgXpDQm.js} +2 -2
- package/dist/plugin-sdk/{outbound-message-hooks-2YmJP9k-.js → outbound-message-hooks-BUHUdpjM.js} +3 -3
- package/dist/plugin-sdk/{path-alias-guards-CHfCMaeX.js → path-alias-guards-CVybi_GQ.js} +1 -1
- package/dist/plugin-sdk/{path-alias-guards-BIPHhakQ.js → path-alias-guards-Dft7g-_v.js} +1 -1
- package/dist/plugin-sdk/{pi-embedded-helpers-CELD3Ns3.js → pi-embedded-helpers-B-OOYT64.js} +11 -11
- package/dist/plugin-sdk/{pi-embedded-helpers-BRymkO_1.js → pi-embedded-helpers-B_jgacW8.js} +11 -11
- package/dist/plugin-sdk/{pi-embedded-utils-AFQ-oCFM.js → pi-embedded-utils-BoSGTbYO.js} +3 -3
- package/dist/plugin-sdk/{pi-embedded-utils-BHmc3RVS.js → pi-embedded-utils-DOXEm_H8.js} +3 -3
- package/dist/plugin-sdk/{pi-model-discovery-C37z3zkx.js → pi-model-discovery-DF6l-F8s.js} +2 -2
- package/dist/plugin-sdk/{pi-model-discovery-6BBajZds.js → pi-model-discovery-DWUrLRLg.js} +1 -1
- package/dist/plugin-sdk/{pi-model-discovery-4X7_2_nP.js → pi-model-discovery-DWw1ReX-.js} +6 -6
- package/dist/plugin-sdk/provider-dispatcher-Byy-KtUn.js +61 -0
- package/dist/plugin-sdk/provider-dispatcher-CAfyX0H-.js +61 -0
- package/dist/plugin-sdk/provider-image-B_dJDOl5.js +22 -0
- package/dist/plugin-sdk/{provider-image-DBye3Ln5.js → provider-image-CEQrHWYz.js} +5 -5
- package/dist/plugin-sdk/provider-image-L_SRDPHU.js +22 -0
- package/dist/plugin-sdk/{provider-image-CbZOYl0b.js → provider-image-zNa1Cccq.js} +5 -5
- package/dist/plugin-sdk/{proxy-DJ2nskkh.js → proxy-BWc9guJs.js} +1 -1
- package/dist/plugin-sdk/{proxy-CjNR_U3u.js → proxy-CXI1ycJj.js} +2 -2
- package/dist/plugin-sdk/proxy-DwbYzxBy.js +7 -0
- package/dist/plugin-sdk/{proxy-env-ubutfW07.js → proxy-env-B3rQCyUO.js} +2 -2
- package/dist/plugin-sdk/{proxy-fetch-CuQpjpkI.js → proxy-fetch-BYkGxQnh.js} +1 -1
- package/dist/plugin-sdk/{proxy-fetch-Cv4b428x.js → proxy-fetch-CNBkczcW.js} +1 -1
- package/dist/plugin-sdk/{pw-ai-BJrhPMHJ.js → pw-ai-DQv6Xhce.js} +10 -10
- package/dist/plugin-sdk/{pw-ai-hVucoF_I.js → pw-ai-JJrE-B1e.js} +10 -10
- package/dist/plugin-sdk/{qmd-manager-Bv5B9pi9.js → qmd-manager-CkQhKIXI.js} +8 -8
- package/dist/plugin-sdk/{qmd-manager-DhW_frb3.js → qmd-manager-CpV0SFnY.js} +8 -8
- package/dist/plugin-sdk/{query-expansion-NpBeKA8F.js → query-expansion-CwmHvvV_.js} +3 -3
- package/dist/plugin-sdk/{query-expansion-CWwH9pqd.js → query-expansion-DjZsVofz.js} +3 -3
- package/dist/plugin-sdk/{recorder-C3qdhG-p.js → recorder-BU_faNrp.js} +4 -4
- package/dist/plugin-sdk/{recorder-DdbAQ2HO.js → recorder-BXZMRSjO.js} +4 -4
- package/dist/plugin-sdk/{redact-CUjmZn2Y.js → redact-C88Ryyfs.js} +1 -1
- package/dist/plugin-sdk/{redact-BEcZiZI1.js → redact-CdcLrd0n.js} +1 -1
- package/dist/plugin-sdk/{registry-BXmLBUZj.js → registry-tm0HFWKN.js} +7 -7
- package/dist/plugin-sdk/replies-B9uDLFOE.js +25 -0
- package/dist/plugin-sdk/replies-BFjGWAPs.js +25 -0
- package/dist/plugin-sdk/{replies-CjeAGcJx.js → replies-CS2GXrd-.js} +4 -4
- package/dist/plugin-sdk/{replies-Dw3jvqpR.js → replies-l2hZAzbt.js} +4 -4
- package/dist/plugin-sdk/{reply-directives-BS8Ge06Z.js → reply-directives-BwxzQXmX.js} +2 -2
- package/dist/plugin-sdk/{reply-directives-OuX5Mba_.js → reply-directives-hfOVgXpA.js} +2 -2
- package/dist/plugin-sdk/{reply-prefix-C83iSHUo.js → reply-prefix-BKQgC6Vj.js} +2 -2
- package/dist/plugin-sdk/reply-prefix-BQMEQam1.js +7 -0
- package/dist/plugin-sdk/{reply-prefix-Dt1hESHJ.js → reply-prefix-qLQRxqrf.js} +1 -1
- package/dist/plugin-sdk/{resolve-route-BpLuk-wC.js → resolve-route-50KoArP9.js} +4 -4
- package/dist/plugin-sdk/{resolve-route-DiQTL1qI.js → resolve-route-6v1HVvWk.js} +6 -6
- package/dist/plugin-sdk/{resolve-route-BLBv69wV.js → resolve-route-tqQv32nr.js} +5 -5
- package/dist/plugin-sdk/runs-BQhYekhr.js +25 -0
- package/dist/plugin-sdk/runs-CPA1-H-2.js +25 -0
- package/dist/plugin-sdk/{runs-D6fAvItC.js → runs-CiohhBb8.js} +7 -7
- package/dist/plugin-sdk/{runs-CPWQbljV.js → runs-H2FY7UEk.js} +8 -8
- package/dist/plugin-sdk/{send-CpXGZgrS.js → send-BMSqETcU.js} +6 -6
- package/dist/plugin-sdk/{send-BwoWjRrJ.js → send-BMhuEEtY.js} +9 -9
- package/dist/plugin-sdk/{send-BUAw8fe4.js → send-BjR1QHnz.js} +7 -7
- package/dist/plugin-sdk/{send-B2Kip2qd.js → send-CEtGyK32.js} +8 -8
- package/dist/plugin-sdk/{send-DE9YP1gQ.js → send-CF3PEjRy.js} +14 -14
- package/dist/plugin-sdk/send-CRtSAH8I.js +25 -0
- package/dist/plugin-sdk/send-CeWqZTK0.js +23 -0
- package/dist/plugin-sdk/{send-BDwTyH6I.js → send-Cv2CGnsK.js} +8 -8
- package/dist/plugin-sdk/{send-Ce2XBaKF.js → send-Cx6gHjnA.js} +6 -6
- package/dist/plugin-sdk/send-CzcKgBBf.js +25 -0
- package/dist/plugin-sdk/send-CzxwgKvR.js +27 -0
- package/dist/plugin-sdk/{send-R0HB1-l-.js → send-DG4MQPPm.js} +13 -13
- package/dist/plugin-sdk/{send-CE_6VOiQ.js → send-DM6RpDCL.js} +7 -7
- package/dist/plugin-sdk/{send-GMYUhBtJ.js → send-DO0C4Uva.js} +19 -19
- package/dist/plugin-sdk/{send-D_2cxLLi.js → send-DUJMd2d7.js} +20 -20
- package/dist/plugin-sdk/{send-CfSEKKlU.js → send-DdPdUcD0.js} +6 -6
- package/dist/plugin-sdk/send-DuCB2xbl.js +23 -0
- package/dist/plugin-sdk/send-Dvna5ey-.js +27 -0
- package/dist/plugin-sdk/send-DyYaWr3E.js +24 -0
- package/dist/plugin-sdk/send-xPV0QIjh.js +24 -0
- package/dist/plugin-sdk/{session-meta-BU2WXD0N.js → session-meta-CiqJP0pO.js} +2 -2
- package/dist/plugin-sdk/session-meta-n4SCcAbJ.js +10 -0
- package/dist/plugin-sdk/{session-meta-D3W538Wj.js → session-meta-oR-S9C5J.js} +3 -3
- package/dist/plugin-sdk/skill-commands-BOV3NIbU.js +14 -0
- package/dist/plugin-sdk/{skill-commands-ByVQek_m.js → skill-commands-Bh43SlDc.js} +6 -6
- package/dist/plugin-sdk/{skill-commands-BYfWIPfV.js → skill-commands-CE_SkRQk.js} +5 -5
- package/dist/plugin-sdk/{skills-BGgry_YW.js → skills-CatBv8W4.js} +6 -6
- package/dist/plugin-sdk/{skills-BuJjEYto.js → skills-CjW6uGTa.js} +6 -6
- package/dist/plugin-sdk/src/config/sessions/run-pending-writes.d.ts +22 -0
- package/dist/plugin-sdk/{ssrf-DiAz1Z_z.js → ssrf-BUCEDxp5.js} +2 -2
- package/dist/plugin-sdk/{store-C6ZLKWUS.js → store-6JKvp2Zu.js} +2 -2
- package/dist/plugin-sdk/{store-Bl3Yjf_w.js → store-yBNTHg3N.js} +2 -2
- package/dist/plugin-sdk/subagent-registry-CZBK6615.js +61 -0
- package/dist/plugin-sdk/subagent-registry-DwnBzoKn.js +61 -0
- package/dist/plugin-sdk/{subsystem-B2x33s9h.js → subsystem-CUUKL6Fr.js} +4 -4
- package/dist/plugin-sdk/{subsystem-0F3fEqJW.js → subsystem-CchgWuXK.js} +4 -4
- package/dist/plugin-sdk/{tables-BCSRuiq_.js → tables-BPCP4CpE.js} +1 -1
- package/dist/plugin-sdk/{tables-CTmQQyfU.js → tables-DKYi6grj.js} +1 -1
- package/dist/plugin-sdk/testing.js +55 -55
- package/dist/plugin-sdk/{tokens-O0pGqOuf.js → tokens-Cioh8xR5.js} +1 -1
- package/dist/plugin-sdk/{tokens-DxJSOoJn.js → tokens-ysZUkYZZ.js} +1 -1
- package/dist/plugin-sdk/{tool-loop-detection-BG8uP1aD.js → tool-loop-detection-BN0lIxLx.js} +1 -1
- package/dist/plugin-sdk/{tool-loop-detection-Buouaxew.js → tool-loop-detection-LsvZ1dIY.js} +1 -1
- package/dist/plugin-sdk/{tools-CmHC6oYK.js → tools-4rcPGSNh.js} +114 -93
- package/dist/plugin-sdk/{user-run-recording-juCW_uQ-.js → user-run-recording-BMaK7jNR.js} +2 -2
- package/dist/plugin-sdk/user-run-recording-DiKcNdjn.js +11 -0
- package/dist/plugin-sdk/{user-run-recording-iS-qLIN9.js → user-run-recording-DlpzQk0X.js} +3 -3
- package/dist/{plugins-cli-DRuMDJWf.js → plugins-cli-Cpq4XXoA.js} +22 -22
- package/dist/{plugins-cli-DnMc_Sn3.js → plugins-cli-yjyVyPjc.js} +22 -22
- package/dist/{program-Gb3aULCK.js → program-CB5O-AyY.js} +23 -23
- package/dist/{program-context-CJuzphrY.js → program-context-DxwZAeg4.js} +24 -24
- package/dist/{prompt-select-styled-Db6swA6j.js → prompt-select-styled-1UYyuvWX.js} +12 -12
- package/dist/{prompt-select-styled-C-FwsHQX.js → prompt-select-styled-CfTLn2TL.js} +12 -12
- package/dist/{provider-image-RxbXQIow.js → provider-image-CZlpEJVI.js} +1 -1
- package/dist/{provider-image-XyDuLsir.js → provider-image-UXLX6qTH.js} +1 -1
- package/dist/{push-apns-BGOaVxug.js → push-apns-DYlVj3Sd.js} +2 -2
- package/dist/{push-apns-Mp5OOpUH.js → push-apns-uBiZYT2e.js} +2 -2
- package/dist/{pw-ai-DmE1ofBV.js → pw-ai-2o8dgziH.js} +1 -1
- package/dist/{pw-ai-QjFlAu_X.js → pw-ai-CZJk6UyG.js} +1 -1
- package/dist/{reconcile-bundled-jiPnEB1a.js → reconcile-bundled-Dx64x8xr.js} +1 -1
- package/dist/{reconcile-bundled-BMWDZl32.js → reconcile-bundled-oQwTCU_A.js} +1 -1
- package/dist/{recovery-adapter-BMqm3Nm4.js → recovery-adapter-BT7EyQzh.js} +8 -8
- package/dist/{recovery-adapter-NtvPZ47M.js → recovery-adapter-BcPtMfaC.js} +1 -1
- package/dist/{recovery-adapter-DvFGPqmC.js → recovery-adapter-BvRiQZIb.js} +8 -8
- package/dist/{recovery-adapter-B_wvSNpI.js → recovery-adapter-CTqGLqLj.js} +1 -1
- package/dist/{register.agent-B5_L0CjL.js → register.agent-DpYKNZhv.js} +22 -22
- package/dist/{register.agent-kUHQvJ5-.js → register.agent-DxsC-Szl.js} +23 -23
- package/dist/{register.configure-BbDac1C1.js → register.configure-DUg0MNnJ.js} +26 -26
- package/dist/{register.configure-CgSKgJn_.js → register.configure-PJZ3F6Y4.js} +26 -26
- package/dist/{register.maintenance-YmHWu7iX.js → register.maintenance-9XyX4HYf.js} +29 -29
- package/dist/{register.maintenance-Co2DeyQ1.js → register.maintenance-BBM-rluy.js} +28 -28
- package/dist/{register.message-8tfreZAx.js → register.message-B3NWvxgd.js} +18 -18
- package/dist/{register.message-BWeca9Pc.js → register.message-D-e6_UXf.js} +18 -18
- package/dist/{register.onboard-Cs4xO4E5.js → register.onboard-CEzQdSxa.js} +2 -2
- package/dist/{register.onboard-4mSx6LtC.js → register.onboard-id-BqwSw.js} +2 -2
- package/dist/{register.setup-BkZyVbO-.js → register.setup-BKBe-Sjp.js} +2 -2
- package/dist/{register.setup-DCRrXbdT.js → register.setup-BiOdr528.js} +2 -2
- package/dist/{register.status-health-sessions-CgAKAaPq.js → register.status-health-sessions-BVIHCaPP.js} +24 -24
- package/dist/{register.status-health-sessions-DK12_BeX.js → register.status-health-sessions-CeRiMlKm.js} +24 -24
- package/dist/{register.subclis-CKq7CiVv.js → register.subclis-IXr-WEhq.js} +16 -16
- package/dist/{reply-BIZ2zaUq.js → reply-BzUdF5qW.js} +69 -24
- package/dist/{run-main-Bjfr1wYn.js → run-main-CsSM1uCJ.js} +30 -30
- package/dist/{runs-Yajg-SST.js → runs-DVNBpgv0.js} +3 -3
- package/dist/{runs-CUs5xDZ_.js → runs-i5IuTzQX.js} +3 -3
- package/dist/{sandbox-DfzTmaAX.js → sandbox-QsMPF2u6.js} +1 -1
- package/dist/{sandbox-Ft7cM-PQ.js → sandbox-bbN1dfIo.js} +1 -1
- package/dist/{sandbox-cli-DvMqiG0-.js → sandbox-cli-CuG1HCrw.js} +2 -2
- package/dist/{sandbox-cli-DyRvKmR5.js → sandbox-cli-nx3ctiYX.js} +2 -2
- package/dist/{security-cli-p_5UwabW.js → security-cli-BfZEQFxV.js} +4 -4
- package/dist/{security-cli-Brok3WFE.js → security-cli-BmfAwh-0.js} +4 -4
- package/dist/{send-DzSRMjOr.js → send-BKkvuHI9.js} +1 -1
- package/dist/{send-Cy2xiH02.js → send-CvkM5xAx.js} +1 -1
- package/dist/{server-l9oy68N_.js → server-BMSgu_SV.js} +4 -4
- package/dist/{server-CywvGQHz.js → server-DRVR_pkr.js} +4 -4
- package/dist/{server-context-Dezs5R_M.js → server-context-Bbi-nqKD.js} +2 -2
- package/dist/{server-context-97Vej0yV.js → server-context-CTilcbO4.js} +2 -2
- package/dist/{server-lifecycle-CHhQugi7.js → server-lifecycle-BIOS9ErD.js} +1 -1
- package/dist/{server-lifecycle-DcVZS1ln.js → server-lifecycle-DrAGY_SC.js} +1 -1
- package/dist/{server-node-events-BG6KvV6u.js → server-node-events-B3c2ctcO.js} +18 -18
- package/dist/{server-node-events-BaFY8QcO.js → server-node-events-lwnuZDvV.js} +18 -18
- package/dist/{service-Hy5BSgmp.js → service-C_LTJsJU.js} +1 -1
- package/dist/{service-RnhuB5cG.js → service-Dkcqg-rd.js} +1 -1
- package/dist/{sessions-BQYQBpX0.js → sessions-Cy_VVMEN.js} +4 -4
- package/dist/{sessions-CuDbhYmU.js → sessions-DCXadUdD.js} +4 -4
- package/dist/{shared-Bc-7t0_-.js → shared-6Uo_6p6X.js} +1 -1
- package/dist/{shared-CJd0QML9.js → shared-Cj18nzyW.js} +1 -1
- package/dist/{status-BYesh62t.js → status-Czr0TmiN.js} +7 -7
- package/dist/{status-DlD1uBWJ.js → status-DJsk1JjI.js} +7 -7
- package/dist/{systemd-B5tdOLBg.js → systemd-BJqlwupF.js} +1 -1
- package/dist/{systemd-DsBMxUv1.js → systemd-D6tSdsgx.js} +1 -1
- package/dist/{systemd-hints-DH86kn6U.js → systemd-hints-Cp0IfOPD.js} +45 -4
- package/dist/{systemd-hints-WHzM5Tf8.js → systemd-hints-DzBfweRH.js} +45 -4
- package/dist/{systemd-linger-BPzLwumk.js → systemd-linger-Dk1GWduD.js} +1 -1
- package/dist/{systemd-linger-EvpJ4ZxF.js → systemd-linger-NvIOU-Ez.js} +1 -1
- package/dist/{task-pipeline-boot-D9T_Earg.js → task-pipeline-boot-CcSZVGUe.js} +17 -17
- package/dist/{task-pipeline-boot-CaVeITev.js → task-pipeline-boot-UcskPjY2.js} +17 -17
- package/dist/{tui-naozJ5Qh.js → tui-WSQKUD4o.js} +1 -1
- package/dist/{tui-cli-hVvCHoLU.js → tui-cli-C3Sopj4_.js} +4 -4
- package/dist/{tui-cli-Dn_Is1pu.js → tui-cli-CpRsjtOO.js} +4 -4
- package/dist/{tui-Bo4LZvgz.js → tui-dccCFhet.js} +1 -1
- package/dist/{update-CXf5cJ0D.js → update-D86-cS_v.js} +1 -1
- package/dist/{update-CAXQiGSa.js → update-DJaR3w6x.js} +1 -1
- package/dist/{update-cli-BB7ybH-w.js → update-cli-B4_xRNut.js} +33 -33
- package/dist/{update-cli-BYDYDEO5.js → update-cli-DDmfG1gm.js} +34 -34
- package/dist/{update-runner-BYv_ec3N.js → update-runner-DqE_9Axt.js} +1 -1
- package/dist/{update-runner-JoI-EXBr.js → update-runner-DwM4xj8C.js} +1 -1
- package/dist/{validation-cli-ZFUB2tzh.js → validation-cli-C8XHeLjI.js} +18 -18
- package/dist/{validation-cli-CGimO4ZJ.js → validation-cli-jSmCMkkm.js} +18 -18
- package/dist/{with-timeout-Cl_OttOS.js → with-timeout-BvxLQUTs.js} +2 -2
- package/dist/{with-timeout-NhJMC1fC.js → with-timeout-CII3PkDO.js} +2 -2
- package/extensions/acpx/package.json +1 -1
- package/extensions/anthropic/package.json +1 -1
- package/extensions/bifrost/package.json +1 -1
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/deepgram/package.json +1 -1
- package/extensions/deepinfra/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/diffs/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/dream-cycle/package.json +1 -1
- package/extensions/elevenlabs/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/gemini-search/package.json +1 -1
- package/extensions/google/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/grok-search/package.json +1 -1
- package/extensions/groq/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/kimi-search/package.json +1 -1
- package/extensions/learning-core/package.json +1 -1
- package/extensions/learning-core/src/bounded-store-reads.conformance.test.ts +23 -411
- package/extensions/learning-core/src/bounded-store-reads.pg.integration.test.ts +52 -0
- package/extensions/learning-core/src/bounded-store-reads.test-helpers.ts +387 -0
- package/extensions/learning-core/src/lesson-store-conformance.pg.integration.test.ts +77 -0
- package/extensions/learning-core/src/lesson-store-conformance.test-helpers.ts +1406 -0
- package/extensions/learning-core/src/lesson-store-conformance.test.ts +27 -1509
- package/extensions/learning-core/src/lesson-store-postgres.test.ts +9 -7
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +1 -1
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/mistral/package.json +1 -1
- package/extensions/moonshot/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +1 -1
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +1 -1
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai/package.json +1 -1
- package/extensions/openai-realtime/package.json +1 -1
- package/extensions/openrouter/package.json +1 -1
- package/extensions/perplexity-search/package.json +1 -1
- package/extensions/searxng-search/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/synology-chat/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +1 -1
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +1 -1
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zai/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +1 -1
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +1 -1
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
- package/dist/plugin-sdk/agent-turn-execute-C4MQxaT2.js +0 -61
- package/dist/plugin-sdk/agent-turn-execute-Cjl_kBtQ.js +0 -61
- package/dist/plugin-sdk/agent-turn-runner-e_lJzDgA.js +0 -11
- package/dist/plugin-sdk/audio-preflight-DE04bi4M.js +0 -28
- package/dist/plugin-sdk/deliver-D4fWK65M.js +0 -32
- package/dist/plugin-sdk/deliver-DuDcszu1.js +0 -32
- package/dist/plugin-sdk/manager-BlYRVNJI.js +0 -16
- package/dist/plugin-sdk/manager-Bqpqu2GS.js +0 -16
- package/dist/plugin-sdk/markdown-tables-cZSKCBuW.js +0 -10
- package/dist/plugin-sdk/provider-dispatcher-D7HHFW68.js +0 -61
- package/dist/plugin-sdk/provider-dispatcher-D7bchiJq.js +0 -61
- package/dist/plugin-sdk/provider-image-Bumd1GBG.js +0 -22
- package/dist/plugin-sdk/provider-image-CNlYbRab.js +0 -22
- package/dist/plugin-sdk/proxy-D2uiu68M.js +0 -7
- package/dist/plugin-sdk/replies-0gHRQjbA.js +0 -25
- package/dist/plugin-sdk/replies-XUkqz_7S.js +0 -25
- package/dist/plugin-sdk/reply-prefix-MU3uY52y.js +0 -7
- package/dist/plugin-sdk/runs-B3DB63tB.js +0 -25
- package/dist/plugin-sdk/runs-R28wdt7j.js +0 -25
- package/dist/plugin-sdk/send-C28pxKhC.js +0 -27
- package/dist/plugin-sdk/send-COofA0BG.js +0 -23
- package/dist/plugin-sdk/send-Cg2VXcO0.js +0 -27
- package/dist/plugin-sdk/send-DAs6FmWB.js +0 -23
- package/dist/plugin-sdk/send-NUHfC-G_.js +0 -25
- package/dist/plugin-sdk/send-Tm5-7bLz.js +0 -24
- package/dist/plugin-sdk/send-UIQH_kN6.js +0 -25
- package/dist/plugin-sdk/send-tASIQF-Q.js +0 -24
- package/dist/plugin-sdk/session-meta-Dd1ZONel.js +0 -10
- package/dist/plugin-sdk/skill-commands-C7ZE3rKj.js +0 -14
- package/dist/plugin-sdk/subagent-registry-2P8D5Trx.js +0 -61
- package/dist/plugin-sdk/subagent-registry-DxZN7H_j.js +0 -61
- package/dist/plugin-sdk/user-run-recording-BMikDBXR.js +0 -11
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { isLearningDomainError } from "./errors.js";
|
|
4
|
+
import { withCandidateDefaults } from "./extraction-candidate.js";
|
|
5
|
+
import { withLessonDefaults } from "./lesson-record.js";
|
|
6
|
+
import type { ExtractionCandidate, LearningLesson, LessonStore, SkillProposal } from "./types.js";
|
|
7
|
+
|
|
8
|
+
// The LessonStore conformance contract, written once and registered per backend leg:
|
|
9
|
+
// lesson-store-conformance.test.ts runs the fs-jsonl, sqlite and in-memory-double legs in the
|
|
10
|
+
// default lanes; lesson-store-conformance.pg.integration.test.ts runs the live-server leg
|
|
11
|
+
// (`pnpm test:pg`).
|
|
12
|
+
|
|
13
|
+
export type StoreScope = { workspaceId?: string; agentId?: string };
|
|
14
|
+
export type MakeStore = (scope: StoreScope) => Promise<LessonStore>;
|
|
15
|
+
|
|
16
|
+
export type Backend = {
|
|
17
|
+
// The backend identity describeBackend() must report. Two entries may share it: the postgres
|
|
18
|
+
// store is run against both a live server and the in-memory double.
|
|
19
|
+
name: "fs-jsonl" | "sqlite" | "postgres";
|
|
20
|
+
// How this leg is named in the suite title.
|
|
21
|
+
label: string;
|
|
22
|
+
make: MakeStore;
|
|
23
|
+
// Raw backing rows (or log bytes), for byte-identity assertions that bypass the store.
|
|
24
|
+
snapshot: () => Promise<string>;
|
|
25
|
+
// This backend's evidence that searchLessons is index-backed (fs-jsonl: reduced capability).
|
|
26
|
+
assertSearchIndex: (store: LessonStore) => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Every workspaceId this suite writes carries the run id, so two concurrent runs against one
|
|
30
|
+
// shared OPENCLAW_DB_URL can neither read nor delete each other's rows
|
|
31
|
+
// (mirrors TEST_SESSION_ID in primary-mode-postgres.pg.integration.test.ts).
|
|
32
|
+
// A pid is unique only among LIVE processes on ONE host: two CI shards, two machines against one
|
|
33
|
+
// shared server, or a later run after pid reuse all collide -- and this suite DELETEs by prefix
|
|
34
|
+
// and DROPs its schema, so a collision is destructive, not merely flaky.
|
|
35
|
+
const RUN_TOKEN = crypto.randomUUID().replaceAll("-", "");
|
|
36
|
+
export const RUN_ID = `test_${RUN_TOKEN}`;
|
|
37
|
+
function ws(name: string): string {
|
|
38
|
+
return `${RUN_ID}_${name}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// `schema` is REQUIRED on PostgresLessonStore; a run-unique name keeps concurrent runs apart.
|
|
42
|
+
export const TEST_SCHEMA = `plugin_learning_core_test_${RUN_TOKEN}`;
|
|
43
|
+
export const PG_TABLE = `${TEST_SCHEMA}.learning_records`;
|
|
44
|
+
|
|
45
|
+
const EXPECTED_CAPABILITY: Record<Backend["name"], "full" | "reduced"> = {
|
|
46
|
+
"fs-jsonl": "reduced",
|
|
47
|
+
sqlite: "full",
|
|
48
|
+
postgres: "full",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// --- Assertion helpers: written ONCE against LessonStore, run for every backend ---
|
|
52
|
+
|
|
53
|
+
async function assertRoundTripsLesson(make: MakeStore) {
|
|
54
|
+
const scope = { agentId: "a1", workspaceId: ws("roundtrip") };
|
|
55
|
+
const store = await make(scope);
|
|
56
|
+
|
|
57
|
+
const lesson: LearningLesson = withLessonDefaults(
|
|
58
|
+
{
|
|
59
|
+
id: "lesson-roundtrip",
|
|
60
|
+
scope,
|
|
61
|
+
kind: "procedure",
|
|
62
|
+
text: "Do X when Y happens.",
|
|
63
|
+
confidence: 0.9,
|
|
64
|
+
occurrences: 1,
|
|
65
|
+
distinctSessionIds: ["session-1"],
|
|
66
|
+
evidence: [
|
|
67
|
+
{
|
|
68
|
+
sessionId: "session-1",
|
|
69
|
+
observedAt: "2026-05-24T10:00:00.000Z",
|
|
70
|
+
summary: "Observed in production.",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
createdAt: "2026-05-24T10:00:00.000Z",
|
|
74
|
+
updatedAt: "2026-05-24T10:00:00.000Z",
|
|
75
|
+
},
|
|
76
|
+
"2026-05-24T10:00:00.000Z",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
await store.upsertLesson(lesson);
|
|
80
|
+
const retrieved = await store.getLesson("lesson-roundtrip");
|
|
81
|
+
|
|
82
|
+
expect(retrieved, "round-trip must preserve every lifecycle field").toEqual(lesson);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The legacy read-through seeds with this: a row another writer already holds (with its distill
|
|
86
|
+
// flags or its invalid_at) must never be overwritten by a stale legacy copy.
|
|
87
|
+
async function assertInsertLessonIfAbsentNeverOverwrites(make: MakeStore) {
|
|
88
|
+
const scope = { agentId: "a1", workspaceId: ws("insert-if-absent") };
|
|
89
|
+
const store = await make(scope);
|
|
90
|
+
const at = "2026-05-24T10:00:00.000Z";
|
|
91
|
+
const lesson: LearningLesson = withLessonDefaults(
|
|
92
|
+
{
|
|
93
|
+
id: "lesson-seed",
|
|
94
|
+
scope,
|
|
95
|
+
kind: "procedure",
|
|
96
|
+
text: "Seed me.",
|
|
97
|
+
confidence: 0.9,
|
|
98
|
+
occurrences: 1,
|
|
99
|
+
distinctSessionIds: ["session-1"],
|
|
100
|
+
evidence: [],
|
|
101
|
+
createdAt: at,
|
|
102
|
+
updatedAt: at,
|
|
103
|
+
},
|
|
104
|
+
at,
|
|
105
|
+
);
|
|
106
|
+
await store.insertLessonIfAbsent(lesson);
|
|
107
|
+
expect(await store.getLesson("lesson-seed")).toEqual(lesson);
|
|
108
|
+
const live = { ...lesson, proposed: true, invalidAt: "2026-05-25T00:00:00.000Z" };
|
|
109
|
+
await store.upsertLesson(live);
|
|
110
|
+
await store.insertLessonIfAbsent(lesson);
|
|
111
|
+
expect(await store.getLesson("lesson-seed")).toEqual(live);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function assertUpsertIsIdempotent(make: MakeStore) {
|
|
115
|
+
const scope = { agentId: "a2", workspaceId: ws("idempotent") };
|
|
116
|
+
const store = await make(scope);
|
|
117
|
+
|
|
118
|
+
const lesson: LearningLesson = withLessonDefaults(
|
|
119
|
+
{
|
|
120
|
+
id: "lesson-idempotent",
|
|
121
|
+
scope,
|
|
122
|
+
kind: "failure",
|
|
123
|
+
text: "Handle error code E_NOENT.",
|
|
124
|
+
confidence: 0.8,
|
|
125
|
+
occurrences: 1,
|
|
126
|
+
distinctSessionIds: ["session-2"],
|
|
127
|
+
evidence: [
|
|
128
|
+
{
|
|
129
|
+
sessionId: "session-2",
|
|
130
|
+
observedAt: "2026-05-24T11:00:00.000Z",
|
|
131
|
+
summary: "Error trace logged.",
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
createdAt: "2026-05-24T11:00:00.000Z",
|
|
135
|
+
updatedAt: "2026-05-24T11:00:00.000Z",
|
|
136
|
+
},
|
|
137
|
+
"2026-05-24T11:00:00.000Z",
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
await store.upsertLesson(lesson);
|
|
141
|
+
await store.upsertLesson(lesson);
|
|
142
|
+
|
|
143
|
+
const all = await store.listLessons();
|
|
144
|
+
expect(all, "a second upsert of the same id must not duplicate the row").toHaveLength(1);
|
|
145
|
+
expect(all[0]).toEqual(lesson);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function assertScopeIsolation(make: MakeStore) {
|
|
149
|
+
const scopeA = { agentId: "a1", workspaceId: ws("w1") };
|
|
150
|
+
const scopeB = { agentId: "a2", workspaceId: ws("w2") };
|
|
151
|
+
const storeW1A1 = await make(scopeA);
|
|
152
|
+
const storeW2A2 = await make(scopeB);
|
|
153
|
+
|
|
154
|
+
const lessonW1A1: LearningLesson = withLessonDefaults(
|
|
155
|
+
{
|
|
156
|
+
id: "lesson-scope-1",
|
|
157
|
+
scope: scopeA,
|
|
158
|
+
kind: "procedure",
|
|
159
|
+
text: "Scoped lesson 1.",
|
|
160
|
+
confidence: 0.7,
|
|
161
|
+
occurrences: 1,
|
|
162
|
+
distinctSessionIds: ["sess-1"],
|
|
163
|
+
evidence: [
|
|
164
|
+
{
|
|
165
|
+
sessionId: "sess-1",
|
|
166
|
+
observedAt: "2026-05-24T12:00:00.000Z",
|
|
167
|
+
summary: "In w1/a1.",
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
createdAt: "2026-05-24T12:00:00.000Z",
|
|
171
|
+
updatedAt: "2026-05-24T12:00:00.000Z",
|
|
172
|
+
},
|
|
173
|
+
"2026-05-24T12:00:00.000Z",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
await storeW1A1.upsertLesson(lessonW1A1);
|
|
177
|
+
|
|
178
|
+
const visibleToW2A2 = await storeW2A2.listLessons();
|
|
179
|
+
const found = visibleToW2A2.find((l) => l.id === lessonW1A1.id);
|
|
180
|
+
// The message names the offending scope so the mutation self-check can match on it instead of
|
|
181
|
+
// on whatever object vitest happens to dump.
|
|
182
|
+
expect(
|
|
183
|
+
found,
|
|
184
|
+
`scope isolation violated: w2 (${scopeB.workspaceId}/${scopeB.agentId}) can read ${lessonW1A1.id}, written under ${scopeA.workspaceId}/${scopeA.agentId}`,
|
|
185
|
+
).toBeUndefined();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// assertScopeIsolation varies workspace AND agent together, so a store that filters on only ONE of
|
|
189
|
+
// the two columns still passes it. This holds each dimension constant in turn, so dropping either
|
|
190
|
+
// predicate is caught. (Found by mutating PostgresLessonStore.scopeFilter to skip agent_id: the
|
|
191
|
+
// combined-scope assertion above stayed green.)
|
|
192
|
+
async function assertScopeIsolationPerDimension(make: MakeStore) {
|
|
193
|
+
const workspaceId = ws("dim");
|
|
194
|
+
|
|
195
|
+
function lessonAt(id: string, scope: StoreScope): LearningLesson {
|
|
196
|
+
return withLessonDefaults(
|
|
197
|
+
{
|
|
198
|
+
id,
|
|
199
|
+
scope,
|
|
200
|
+
kind: "procedure",
|
|
201
|
+
text: `Lesson ${id}.`,
|
|
202
|
+
confidence: 0.7,
|
|
203
|
+
occurrences: 1,
|
|
204
|
+
distinctSessionIds: ["sess-dim"],
|
|
205
|
+
evidence: [
|
|
206
|
+
{ sessionId: "sess-dim", observedAt: "2026-05-24T12:00:00.000Z", summary: "dim." },
|
|
207
|
+
],
|
|
208
|
+
createdAt: "2026-05-24T12:00:00.000Z",
|
|
209
|
+
updatedAt: "2026-05-24T12:00:00.000Z",
|
|
210
|
+
},
|
|
211
|
+
"2026-05-24T12:00:00.000Z",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Same workspace, different agent — only the agent_id predicate can separate these.
|
|
216
|
+
const agentA = { workspaceId, agentId: "dim-a1" };
|
|
217
|
+
const agentB = { workspaceId, agentId: "dim-a2" };
|
|
218
|
+
await (await make(agentA)).upsertLesson(lessonAt("lesson-dim-agent", agentA));
|
|
219
|
+
const seenByOtherAgent = (await (await make(agentB)).listLessons()).find(
|
|
220
|
+
(l) => l.id === "lesson-dim-agent",
|
|
221
|
+
);
|
|
222
|
+
expect(
|
|
223
|
+
seenByOtherAgent,
|
|
224
|
+
`agent isolation violated: agent ${agentB.agentId} can read a lesson written by ${agentA.agentId} in the SAME workspace ${workspaceId}`,
|
|
225
|
+
).toBeUndefined();
|
|
226
|
+
|
|
227
|
+
// Same agent, different workspace — only the workspace_id predicate can separate these.
|
|
228
|
+
const wsA = { workspaceId: ws("dim-w1"), agentId: "dim-shared" };
|
|
229
|
+
const wsB = { workspaceId: ws("dim-w2"), agentId: "dim-shared" };
|
|
230
|
+
await (await make(wsA)).upsertLesson(lessonAt("lesson-dim-workspace", wsA));
|
|
231
|
+
const seenByOtherWorkspace = (await (await make(wsB)).listLessons()).find(
|
|
232
|
+
(l) => l.id === "lesson-dim-workspace",
|
|
233
|
+
);
|
|
234
|
+
expect(
|
|
235
|
+
seenByOtherWorkspace,
|
|
236
|
+
`workspace isolation violated: workspace ${wsB.workspaceId} can read a lesson written in ${wsA.workspaceId} by the SAME agent ${wsA.agentId}`,
|
|
237
|
+
).toBeUndefined();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function assertEmptyScopeReadsAcrossScopes(make: MakeStore) {
|
|
241
|
+
const scopeA = { agentId: "a1", workspaceId: ws("cross") };
|
|
242
|
+
const storeW1A1 = await make(scopeA);
|
|
243
|
+
const storeEmptyScope = await make({});
|
|
244
|
+
|
|
245
|
+
const lessonW1A1: LearningLesson = withLessonDefaults(
|
|
246
|
+
{
|
|
247
|
+
id: "lesson-cross-scope",
|
|
248
|
+
scope: scopeA,
|
|
249
|
+
kind: "procedure",
|
|
250
|
+
text: "Lesson visible across scopes.",
|
|
251
|
+
confidence: 0.75,
|
|
252
|
+
occurrences: 1,
|
|
253
|
+
distinctSessionIds: ["sess-2"],
|
|
254
|
+
evidence: [
|
|
255
|
+
{
|
|
256
|
+
sessionId: "sess-2",
|
|
257
|
+
observedAt: "2026-05-24T13:00:00.000Z",
|
|
258
|
+
summary: "Cross-scope test.",
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
createdAt: "2026-05-24T13:00:00.000Z",
|
|
262
|
+
updatedAt: "2026-05-24T13:00:00.000Z",
|
|
263
|
+
},
|
|
264
|
+
"2026-05-24T13:00:00.000Z",
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await storeW1A1.upsertLesson(lessonW1A1);
|
|
268
|
+
|
|
269
|
+
const allLessons = await storeEmptyScope.listLessons();
|
|
270
|
+
expect(
|
|
271
|
+
allLessons.find((l) => l.id === lessonW1A1.id),
|
|
272
|
+
"an empty scope must read across every scope",
|
|
273
|
+
).toBeDefined();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function assertSupersedeRetainsPredecessor(make: MakeStore) {
|
|
277
|
+
const scope = { agentId: "a3", workspaceId: ws("supersede") };
|
|
278
|
+
const store = await make(scope);
|
|
279
|
+
|
|
280
|
+
const predecessor: LearningLesson = withLessonDefaults(
|
|
281
|
+
{
|
|
282
|
+
id: "lesson-v1",
|
|
283
|
+
scope,
|
|
284
|
+
kind: "procedure",
|
|
285
|
+
text: "Old approach.",
|
|
286
|
+
confidence: 0.6,
|
|
287
|
+
occurrences: 1,
|
|
288
|
+
distinctSessionIds: ["sess-3"],
|
|
289
|
+
evidence: [
|
|
290
|
+
{
|
|
291
|
+
sessionId: "sess-3",
|
|
292
|
+
observedAt: "2026-05-24T14:00:00.000Z",
|
|
293
|
+
summary: "Original approach.",
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
createdAt: "2026-05-24T14:00:00.000Z",
|
|
297
|
+
updatedAt: "2026-05-24T14:00:00.000Z",
|
|
298
|
+
},
|
|
299
|
+
"2026-05-24T14:00:00.000Z",
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
await store.upsertLesson(predecessor);
|
|
303
|
+
|
|
304
|
+
const successor: LearningLesson = withLessonDefaults(
|
|
305
|
+
{
|
|
306
|
+
id: "lesson-v2",
|
|
307
|
+
scope,
|
|
308
|
+
kind: "procedure",
|
|
309
|
+
text: "New improved approach.",
|
|
310
|
+
confidence: 0.9,
|
|
311
|
+
occurrences: 1,
|
|
312
|
+
distinctSessionIds: ["sess-3"],
|
|
313
|
+
evidence: [
|
|
314
|
+
{
|
|
315
|
+
sessionId: "sess-3",
|
|
316
|
+
observedAt: "2026-05-24T14:15:00.000Z",
|
|
317
|
+
summary: "Improved version.",
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
createdAt: "2026-05-24T14:15:00.000Z",
|
|
321
|
+
updatedAt: "2026-05-24T14:15:00.000Z",
|
|
322
|
+
},
|
|
323
|
+
"2026-05-24T14:15:00.000Z",
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const result = await store.supersedeLesson({
|
|
327
|
+
predecessorId: "lesson-v1",
|
|
328
|
+
successor,
|
|
329
|
+
now: "2026-05-24T14:15:00.000Z",
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(result.supersedesId).toBe("lesson-v1");
|
|
333
|
+
|
|
334
|
+
const pred = await store.getLesson("lesson-v1");
|
|
335
|
+
expect(pred, "supersede must retain the predecessor").toBeDefined();
|
|
336
|
+
expect(pred?.invalidAt, "the predecessor must carry invalid_at").toBe("2026-05-24T14:15:00.000Z");
|
|
337
|
+
|
|
338
|
+
const succ = await store.getLesson("lesson-v2");
|
|
339
|
+
expect(succ?.supersedesId, "the successor must link back to the predecessor").toBe("lesson-v1");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// A successor sharing the predecessor's id would overwrite the same primary key with a row whose
|
|
343
|
+
// supersedesId points at itself, and every later getLessonChain would throw "cycle detected" -- an
|
|
344
|
+
// unrecoverable break in the provenance guarantee. The tool derives the successor id by hashing the
|
|
345
|
+
// text, so identical text reaches this path from a real operator action, not just a bad caller.
|
|
346
|
+
// The ordering contract. Backends store records in different physical layouts (append-only log
|
|
347
|
+
// position, SQLite rowid, a Postgres heap row), and an UPDATE perturbs each layout differently:
|
|
348
|
+
// `INSERT OR REPLACE` mints a new rowid, the log's dedup keeps the newest globalSeq, while a real
|
|
349
|
+
// `ON CONFLICT DO UPDATE` leaves the row where it was. Ordering therefore has to be stated on the
|
|
350
|
+
// data (createdAt, id tiebreak), not inherited from storage. gateway-methods.ts hands list output
|
|
351
|
+
// straight to the operator with no sort, so a divergence here is directly user-visible.
|
|
352
|
+
//
|
|
353
|
+
// The update must target the EARLIER record, after the later one exists. Updating the most recent
|
|
354
|
+
// record leaves it last under every layout, so the assertion would pass on all three backends while
|
|
355
|
+
// the divergence was fully present.
|
|
356
|
+
async function assertUpdateDoesNotReorder(make: MakeStore) {
|
|
357
|
+
const scope = { agentId: "a4order", workspaceId: ws("order") };
|
|
358
|
+
const store = await make(scope);
|
|
359
|
+
const early = "2026-05-24T09:00:00.000Z";
|
|
360
|
+
const late = "2026-05-24T17:00:00.000Z";
|
|
361
|
+
|
|
362
|
+
function at(
|
|
363
|
+
id: string,
|
|
364
|
+
createdAt: string,
|
|
365
|
+
text: string,
|
|
366
|
+
evidence: LearningLesson["evidence"] = [],
|
|
367
|
+
): LearningLesson {
|
|
368
|
+
return withLessonDefaults(
|
|
369
|
+
{
|
|
370
|
+
id,
|
|
371
|
+
scope,
|
|
372
|
+
kind: "procedure",
|
|
373
|
+
text,
|
|
374
|
+
confidence: 0.7,
|
|
375
|
+
occurrences: 1,
|
|
376
|
+
distinctSessionIds: ["sess-order"],
|
|
377
|
+
evidence,
|
|
378
|
+
createdAt,
|
|
379
|
+
updatedAt: createdAt,
|
|
380
|
+
},
|
|
381
|
+
createdAt,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
await store.upsertLesson(at("lesson-early", early, "first"));
|
|
386
|
+
await store.upsertLesson(at("lesson-late", late, "second"));
|
|
387
|
+
expect((await store.listLessons()).map((l) => l.id)).toEqual(["lesson-early", "lesson-late"]);
|
|
388
|
+
|
|
389
|
+
// The merge must genuinely MUTATE the record. mergeLessons ignores candidate.text and derives
|
|
390
|
+
// every change from candidate.evidence, so a candidate with no new evidence is a no-op: the
|
|
391
|
+
// fs-jsonl store emits no delta, appends nothing, and the list cannot reorder — the assertion
|
|
392
|
+
// below would then pass on every backend with the sort removed. Supplying unseen evidence is what
|
|
393
|
+
// makes this leg able to fail.
|
|
394
|
+
await store.mergeLesson(
|
|
395
|
+
at("lesson-early", early, "first", [
|
|
396
|
+
{ sessionId: "sess-order-2", observedAt: late, summary: "a later observation" },
|
|
397
|
+
]),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
expect(
|
|
401
|
+
(await store.listLessons()).map((l) => l.id),
|
|
402
|
+
"updating the EARLIER lesson must not move it in the list",
|
|
403
|
+
).toEqual(["lesson-early", "lesson-late"]);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function assertSupersedeRejectsSelfReference(make: MakeStore) {
|
|
407
|
+
const scope = { agentId: "a3self", workspaceId: ws("supersede-self") };
|
|
408
|
+
const store = await make(scope);
|
|
409
|
+
const now = "2026-05-24T14:15:00.000Z";
|
|
410
|
+
|
|
411
|
+
await store.upsertLesson(
|
|
412
|
+
withLessonDefaults(
|
|
413
|
+
{
|
|
414
|
+
id: "lesson-self",
|
|
415
|
+
scope,
|
|
416
|
+
kind: "correction",
|
|
417
|
+
text: "original",
|
|
418
|
+
confidence: 0.9,
|
|
419
|
+
occurrences: 1,
|
|
420
|
+
distinctSessionIds: ["sess-self"],
|
|
421
|
+
evidence: [],
|
|
422
|
+
createdAt: now,
|
|
423
|
+
updatedAt: now,
|
|
424
|
+
},
|
|
425
|
+
now,
|
|
426
|
+
),
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
await expect(
|
|
430
|
+
store.supersedeLesson({
|
|
431
|
+
predecessorId: "lesson-self",
|
|
432
|
+
successor: withLessonDefaults(
|
|
433
|
+
{
|
|
434
|
+
id: "lesson-self",
|
|
435
|
+
scope,
|
|
436
|
+
kind: "correction",
|
|
437
|
+
text: "original",
|
|
438
|
+
confidence: 0.9,
|
|
439
|
+
occurrences: 1,
|
|
440
|
+
distinctSessionIds: ["sess-self"],
|
|
441
|
+
evidence: [],
|
|
442
|
+
createdAt: now,
|
|
443
|
+
updatedAt: now,
|
|
444
|
+
},
|
|
445
|
+
now,
|
|
446
|
+
),
|
|
447
|
+
now,
|
|
448
|
+
}),
|
|
449
|
+
"a successor sharing the predecessor id must be refused, not written",
|
|
450
|
+
).rejects.toThrow(/must differ from the predecessor/);
|
|
451
|
+
|
|
452
|
+
// The refusal must leave the predecessor untouched -- a partial write is the corruption itself.
|
|
453
|
+
const pred = await store.getLesson("lesson-self");
|
|
454
|
+
expect(
|
|
455
|
+
pred?.invalidAt,
|
|
456
|
+
"the refused supersede must not have closed the predecessor",
|
|
457
|
+
).toBeUndefined();
|
|
458
|
+
expect(
|
|
459
|
+
pred?.supersedesId,
|
|
460
|
+
"the refused supersede must not have self-linked the record",
|
|
461
|
+
).toBeUndefined();
|
|
462
|
+
await expect(
|
|
463
|
+
store.getLessonChain("lesson-self"),
|
|
464
|
+
"the chain must still be readable after the refusal",
|
|
465
|
+
).resolves.toBeDefined();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function assertActiveListExcludesInvalidated(make: MakeStore) {
|
|
469
|
+
const scope = { agentId: "a4", workspaceId: ws("active") };
|
|
470
|
+
const store = await make(scope);
|
|
471
|
+
|
|
472
|
+
const lesson: LearningLesson = withLessonDefaults(
|
|
473
|
+
{
|
|
474
|
+
id: "lesson-active",
|
|
475
|
+
scope,
|
|
476
|
+
kind: "preference",
|
|
477
|
+
text: "Active lesson.",
|
|
478
|
+
confidence: 0.8,
|
|
479
|
+
occurrences: 1,
|
|
480
|
+
distinctSessionIds: ["sess-4"],
|
|
481
|
+
evidence: [
|
|
482
|
+
{
|
|
483
|
+
sessionId: "sess-4",
|
|
484
|
+
observedAt: "2026-05-24T15:00:00.000Z",
|
|
485
|
+
summary: "Active.",
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
createdAt: "2026-05-24T15:00:00.000Z",
|
|
489
|
+
updatedAt: "2026-05-24T15:00:00.000Z",
|
|
490
|
+
},
|
|
491
|
+
"2026-05-24T15:00:00.000Z",
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
await store.upsertLesson(lesson);
|
|
495
|
+
|
|
496
|
+
const invalidLesson: LearningLesson = withLessonDefaults(
|
|
497
|
+
{
|
|
498
|
+
id: "lesson-invalid",
|
|
499
|
+
scope,
|
|
500
|
+
kind: "preference",
|
|
501
|
+
text: "Invalidated lesson.",
|
|
502
|
+
confidence: 0.5,
|
|
503
|
+
occurrences: 1,
|
|
504
|
+
distinctSessionIds: ["sess-4"],
|
|
505
|
+
evidence: [
|
|
506
|
+
{
|
|
507
|
+
sessionId: "sess-4",
|
|
508
|
+
observedAt: "2026-05-24T15:30:00.000Z",
|
|
509
|
+
summary: "Invalid.",
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
createdAt: "2026-05-24T15:30:00.000Z",
|
|
513
|
+
updatedAt: "2026-05-24T15:30:00.000Z",
|
|
514
|
+
invalidAt: "2026-05-24T16:00:00.000Z",
|
|
515
|
+
},
|
|
516
|
+
"2026-05-24T15:30:00.000Z",
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
await store.upsertLesson(invalidLesson);
|
|
520
|
+
|
|
521
|
+
expect(await store.getLesson("lesson-active")).toBeDefined();
|
|
522
|
+
expect(
|
|
523
|
+
await store.getLesson("lesson-invalid"),
|
|
524
|
+
"getLesson must still return an invalidated record",
|
|
525
|
+
).toBeDefined();
|
|
526
|
+
|
|
527
|
+
// BOTH ends of the window, not just invalidAt: a record whose validFrom has not arrived is not
|
|
528
|
+
// active either, and only asserting the expiry end would leave that half uncovered per backend.
|
|
529
|
+
const notYetValid: LearningLesson = withLessonDefaults(
|
|
530
|
+
{
|
|
531
|
+
id: "lesson-not-yet-valid",
|
|
532
|
+
scope,
|
|
533
|
+
kind: "preference",
|
|
534
|
+
text: "Not yet effective.",
|
|
535
|
+
confidence: 0.5,
|
|
536
|
+
occurrences: 1,
|
|
537
|
+
distinctSessionIds: ["sess-4"],
|
|
538
|
+
evidence: [],
|
|
539
|
+
createdAt: "2026-05-24T15:30:00.000Z",
|
|
540
|
+
updatedAt: "2026-05-24T15:30:00.000Z",
|
|
541
|
+
validFrom: "2099-01-01T00:00:00.000Z",
|
|
542
|
+
},
|
|
543
|
+
"2026-05-24T15:30:00.000Z",
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
await store.upsertLesson(notYetValid);
|
|
547
|
+
|
|
548
|
+
const now = "2026-05-24T17:00:00.000Z";
|
|
549
|
+
const active = await store.listActiveLessons(now);
|
|
550
|
+
expect(active.find((l) => l.id === "lesson-active")).toBeDefined();
|
|
551
|
+
expect(
|
|
552
|
+
active.find((l) => l.id === "lesson-invalid"),
|
|
553
|
+
"listActiveLessons must exclude an invalidated record",
|
|
554
|
+
).toBeUndefined();
|
|
555
|
+
expect(
|
|
556
|
+
active.find((l) => l.id === "lesson-not-yet-valid"),
|
|
557
|
+
"listActiveLessons must exclude a record whose validFrom has not arrived",
|
|
558
|
+
).toBeUndefined();
|
|
559
|
+
expect(
|
|
560
|
+
await store.getLesson("lesson-not-yet-valid"),
|
|
561
|
+
"getLesson must still return a not-yet-valid record",
|
|
562
|
+
).toBeDefined();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function assertChainWalksNewestFirstAndRejectsCycle(make: MakeStore) {
|
|
566
|
+
const scope = { agentId: "a5", workspaceId: ws("chain") };
|
|
567
|
+
const store = await make(scope);
|
|
568
|
+
|
|
569
|
+
const v1: LearningLesson = withLessonDefaults(
|
|
570
|
+
{
|
|
571
|
+
id: "lesson-v1-chain",
|
|
572
|
+
scope,
|
|
573
|
+
kind: "procedure",
|
|
574
|
+
text: "Version 1.",
|
|
575
|
+
confidence: 0.5,
|
|
576
|
+
occurrences: 1,
|
|
577
|
+
distinctSessionIds: ["sess-5"],
|
|
578
|
+
evidence: [
|
|
579
|
+
{
|
|
580
|
+
sessionId: "sess-5",
|
|
581
|
+
observedAt: "2026-05-24T17:00:00.000Z",
|
|
582
|
+
summary: "V1.",
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
createdAt: "2026-05-24T17:00:00.000Z",
|
|
586
|
+
updatedAt: "2026-05-24T17:00:00.000Z",
|
|
587
|
+
},
|
|
588
|
+
"2026-05-24T17:00:00.000Z",
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
await store.upsertLesson(v1);
|
|
592
|
+
|
|
593
|
+
await store.supersedeLesson({
|
|
594
|
+
predecessorId: "lesson-v1-chain",
|
|
595
|
+
successor: withLessonDefaults(
|
|
596
|
+
{
|
|
597
|
+
id: "lesson-v2-chain",
|
|
598
|
+
scope,
|
|
599
|
+
kind: "procedure",
|
|
600
|
+
text: "Version 2.",
|
|
601
|
+
confidence: 0.7,
|
|
602
|
+
occurrences: 1,
|
|
603
|
+
distinctSessionIds: ["sess-5"],
|
|
604
|
+
evidence: [
|
|
605
|
+
{
|
|
606
|
+
sessionId: "sess-5",
|
|
607
|
+
observedAt: "2026-05-24T17:30:00.000Z",
|
|
608
|
+
summary: "V2.",
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
createdAt: "2026-05-24T17:30:00.000Z",
|
|
612
|
+
updatedAt: "2026-05-24T17:30:00.000Z",
|
|
613
|
+
},
|
|
614
|
+
"2026-05-24T17:30:00.000Z",
|
|
615
|
+
),
|
|
616
|
+
now: "2026-05-24T17:30:00.000Z",
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
await store.supersedeLesson({
|
|
620
|
+
predecessorId: "lesson-v2-chain",
|
|
621
|
+
successor: withLessonDefaults(
|
|
622
|
+
{
|
|
623
|
+
id: "lesson-v3-chain",
|
|
624
|
+
scope,
|
|
625
|
+
kind: "procedure",
|
|
626
|
+
text: "Version 3.",
|
|
627
|
+
confidence: 0.95,
|
|
628
|
+
occurrences: 1,
|
|
629
|
+
distinctSessionIds: ["sess-5"],
|
|
630
|
+
evidence: [
|
|
631
|
+
{
|
|
632
|
+
sessionId: "sess-5",
|
|
633
|
+
observedAt: "2026-05-24T18:00:00.000Z",
|
|
634
|
+
summary: "V3.",
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
createdAt: "2026-05-24T18:00:00.000Z",
|
|
638
|
+
updatedAt: "2026-05-24T18:00:00.000Z",
|
|
639
|
+
},
|
|
640
|
+
"2026-05-24T18:00:00.000Z",
|
|
641
|
+
),
|
|
642
|
+
now: "2026-05-24T18:00:00.000Z",
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const chain = await store.getLessonChain("lesson-v3-chain");
|
|
646
|
+
expect(
|
|
647
|
+
chain.map((lesson) => lesson.id),
|
|
648
|
+
"getLessonChain must walk newest-first",
|
|
649
|
+
).toEqual(["lesson-v3-chain", "lesson-v2-chain", "lesson-v1-chain"]);
|
|
650
|
+
|
|
651
|
+
// A corrupt pair of mutual back-links must be rejected, not walked forever. supersedeLesson
|
|
652
|
+
// cannot produce this, so the cycle is written through upsertLesson.
|
|
653
|
+
const cycleA: LearningLesson = withLessonDefaults(
|
|
654
|
+
{
|
|
655
|
+
id: "lesson-cycle-a",
|
|
656
|
+
scope,
|
|
657
|
+
supersedesId: "lesson-cycle-b",
|
|
658
|
+
kind: "procedure",
|
|
659
|
+
text: "Cycle A.",
|
|
660
|
+
confidence: 0.5,
|
|
661
|
+
occurrences: 1,
|
|
662
|
+
distinctSessionIds: ["sess-5"],
|
|
663
|
+
evidence: [
|
|
664
|
+
{
|
|
665
|
+
sessionId: "sess-5",
|
|
666
|
+
observedAt: "2026-05-24T18:30:00.000Z",
|
|
667
|
+
summary: "Cycle A.",
|
|
668
|
+
},
|
|
669
|
+
],
|
|
670
|
+
createdAt: "2026-05-24T18:30:00.000Z",
|
|
671
|
+
updatedAt: "2026-05-24T18:30:00.000Z",
|
|
672
|
+
},
|
|
673
|
+
"2026-05-24T18:30:00.000Z",
|
|
674
|
+
);
|
|
675
|
+
const cycleB: LearningLesson = withLessonDefaults(
|
|
676
|
+
{
|
|
677
|
+
id: "lesson-cycle-b",
|
|
678
|
+
scope,
|
|
679
|
+
supersedesId: "lesson-cycle-a",
|
|
680
|
+
kind: "procedure",
|
|
681
|
+
text: "Cycle B.",
|
|
682
|
+
confidence: 0.5,
|
|
683
|
+
occurrences: 1,
|
|
684
|
+
distinctSessionIds: ["sess-5"],
|
|
685
|
+
evidence: [
|
|
686
|
+
{
|
|
687
|
+
sessionId: "sess-5",
|
|
688
|
+
observedAt: "2026-05-24T18:31:00.000Z",
|
|
689
|
+
summary: "Cycle B.",
|
|
690
|
+
},
|
|
691
|
+
],
|
|
692
|
+
createdAt: "2026-05-24T18:31:00.000Z",
|
|
693
|
+
updatedAt: "2026-05-24T18:31:00.000Z",
|
|
694
|
+
},
|
|
695
|
+
"2026-05-24T18:31:00.000Z",
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
await store.upsertLesson(cycleA);
|
|
699
|
+
await store.upsertLesson(cycleB);
|
|
700
|
+
|
|
701
|
+
await expect(store.getLessonChain("lesson-cycle-a")).rejects.toThrow(/cycle/i);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function assertMergeAccumulatesOccurrencesAndEvidence(make: MakeStore) {
|
|
705
|
+
const scope = { agentId: "a7", workspaceId: ws("merge") };
|
|
706
|
+
const store = await make(scope);
|
|
707
|
+
|
|
708
|
+
const lesson1: LearningLesson = withLessonDefaults(
|
|
709
|
+
{
|
|
710
|
+
id: "lesson-merge",
|
|
711
|
+
scope,
|
|
712
|
+
kind: "failure",
|
|
713
|
+
text: "Handle timeouts.",
|
|
714
|
+
confidence: 0.7,
|
|
715
|
+
occurrences: 1,
|
|
716
|
+
distinctSessionIds: ["sess-7a"],
|
|
717
|
+
evidence: [
|
|
718
|
+
{
|
|
719
|
+
evidenceId: "ev-1",
|
|
720
|
+
sessionId: "sess-7a",
|
|
721
|
+
observedAt: "2026-05-24T19:00:00.000Z",
|
|
722
|
+
summary: "First occurrence.",
|
|
723
|
+
},
|
|
724
|
+
],
|
|
725
|
+
createdAt: "2026-05-24T19:00:00.000Z",
|
|
726
|
+
updatedAt: "2026-05-24T19:00:00.000Z",
|
|
727
|
+
},
|
|
728
|
+
"2026-05-24T19:00:00.000Z",
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
const merged1 = await store.mergeLesson(lesson1);
|
|
732
|
+
expect(merged1.occurrences).toBe(1);
|
|
733
|
+
expect(merged1.evidence).toHaveLength(1);
|
|
734
|
+
|
|
735
|
+
const lesson2: LearningLesson = withLessonDefaults(
|
|
736
|
+
{
|
|
737
|
+
id: "lesson-merge",
|
|
738
|
+
scope,
|
|
739
|
+
kind: "failure",
|
|
740
|
+
text: "Handle timeouts.",
|
|
741
|
+
confidence: 0.85,
|
|
742
|
+
occurrences: 1,
|
|
743
|
+
distinctSessionIds: ["sess-7b"],
|
|
744
|
+
evidence: [
|
|
745
|
+
{
|
|
746
|
+
evidenceId: "ev-2",
|
|
747
|
+
sessionId: "sess-7b",
|
|
748
|
+
observedAt: "2026-05-24T19:30:00.000Z",
|
|
749
|
+
summary: "Second occurrence.",
|
|
750
|
+
},
|
|
751
|
+
],
|
|
752
|
+
createdAt: "2026-05-24T19:00:00.000Z",
|
|
753
|
+
updatedAt: "2026-05-24T19:30:00.000Z",
|
|
754
|
+
},
|
|
755
|
+
"2026-05-24T19:00:00.000Z",
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
const merged2 = await store.mergeLesson(lesson2);
|
|
759
|
+
expect(merged2.occurrences, "merge must accumulate occurrences").toBe(2);
|
|
760
|
+
expect(merged2.evidence, "merge must accumulate evidence").toHaveLength(2);
|
|
761
|
+
expect(merged2.distinctSessionIds).toEqual(["sess-7a", "sess-7b"]);
|
|
762
|
+
expect(merged2.confidence).toBe(0.85);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async function assertProposalAndAuditRoundTrip(make: MakeStore) {
|
|
766
|
+
const store = await make({ agentId: "a8", workspaceId: ws("proposal") });
|
|
767
|
+
|
|
768
|
+
const proposal: SkillProposal = {
|
|
769
|
+
id: "proposal-1",
|
|
770
|
+
lessonIds: ["lesson-1"],
|
|
771
|
+
status: "pending",
|
|
772
|
+
skillName: "test-skill",
|
|
773
|
+
title: "Test proposal",
|
|
774
|
+
body: "# Test",
|
|
775
|
+
scannerFindings: [],
|
|
776
|
+
createdAt: "2026-05-24T20:00:00.000Z",
|
|
777
|
+
updatedAt: "2026-05-24T20:00:00.000Z",
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
await store.insertProposalIfAbsent(proposal);
|
|
781
|
+
const retrieved = await store.getProposal("proposal-1");
|
|
782
|
+
expect(retrieved).toEqual(proposal);
|
|
783
|
+
|
|
784
|
+
const decided = await store.recordProposalDecision({
|
|
785
|
+
proposalId: "proposal-1",
|
|
786
|
+
status: "applied",
|
|
787
|
+
actor: "test-user",
|
|
788
|
+
now: "2026-05-24T20:15:00.000Z",
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
expect(decided.status).toBe("applied");
|
|
792
|
+
expect(decided.decidedBy).toBeDefined();
|
|
793
|
+
expect(decided.decidedAt).toBe("2026-05-24T20:15:00.000Z");
|
|
794
|
+
|
|
795
|
+
const audit = await store.listAudit();
|
|
796
|
+
expect(audit.length, "the decision must append an audit event").toBeGreaterThan(0);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async function assertFindProposalWithScope(make: MakeStore) {
|
|
800
|
+
const scope = { agentId: "a9", workspaceId: ws("proposal-scope") };
|
|
801
|
+
const store = await make(scope);
|
|
802
|
+
|
|
803
|
+
const proposal: SkillProposal = {
|
|
804
|
+
id: "proposal-scoped",
|
|
805
|
+
lessonIds: ["lesson-1"],
|
|
806
|
+
status: "pending",
|
|
807
|
+
skillName: "test-skill",
|
|
808
|
+
title: "Test proposal",
|
|
809
|
+
body: "# Test",
|
|
810
|
+
scannerFindings: [],
|
|
811
|
+
createdAt: "2026-05-24T20:00:00.000Z",
|
|
812
|
+
updatedAt: "2026-05-24T20:00:00.000Z",
|
|
813
|
+
};
|
|
814
|
+
await store.insertProposalIfAbsent(proposal);
|
|
815
|
+
|
|
816
|
+
// Look it up through a store built with an EMPTY scope. That is exactly how the gateway calls it
|
|
817
|
+
// (index.ts wires `createLessonStore({ ..., scope: {} })`), and it is the leg that fails today.
|
|
818
|
+
const crossScope = await make({});
|
|
819
|
+
const found = await crossScope.findProposalWithScope("proposal-scoped");
|
|
820
|
+
expect(
|
|
821
|
+
found,
|
|
822
|
+
"a proposal written under one scope must be findable from an empty scope",
|
|
823
|
+
).toBeDefined();
|
|
824
|
+
expect(found?.proposal.id).toBe("proposal-scoped");
|
|
825
|
+
// The proposal's OWN scope must come back -- NOT the caller's. If this returns the empty scope,
|
|
826
|
+
// decide() resolves no workspace and apply dies with "workspace unresolvable; cannot apply".
|
|
827
|
+
expect(found?.scope.workspaceId).toBe(scope.workspaceId);
|
|
828
|
+
expect(found?.scope.agentId).toBe(scope.agentId);
|
|
829
|
+
|
|
830
|
+
// A proposal written WITHOUT a scope must read back as an empty scope on EVERY backend. The SQL
|
|
831
|
+
// stores persist an absent scope field as "" (writeRecord: `this.scope.workspaceId ?? ""`), never
|
|
832
|
+
// NULL, so a column read that only guards against NULL hands back {workspaceId: "", agentId: ""}
|
|
833
|
+
// and diverges from fs-jsonl, which returns the raw log scope `{}`.
|
|
834
|
+
const unscopedStore = await make({});
|
|
835
|
+
await unscopedStore.insertProposalIfAbsent({
|
|
836
|
+
...proposal,
|
|
837
|
+
id: "proposal-unscoped",
|
|
838
|
+
lessonIds: ["lesson-2"],
|
|
839
|
+
skillName: "test-skill-unscoped",
|
|
840
|
+
});
|
|
841
|
+
const unscoped = await crossScope.findProposalWithScope("proposal-unscoped");
|
|
842
|
+
expect(unscoped?.proposal.id).toBe("proposal-unscoped");
|
|
843
|
+
expect(unscoped?.scope).toEqual({});
|
|
844
|
+
|
|
845
|
+
expect(await crossScope.findProposalWithScope("no-such-proposal")).toBeUndefined();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function assertSupersedeRejectsOccupiedSuccessorId(make: MakeStore) {
|
|
849
|
+
// The tool derives the successor id from the replacement TEXT, so text that duplicates another
|
|
850
|
+
// lesson's text mints THAT lesson's id. Writing it would overwrite an unrelated record with the
|
|
851
|
+
// predecessor's carried fields and still report success -- silent cross-lesson data loss.
|
|
852
|
+
const scope = { agentId: "a1", workspaceId: ws("occupied-successor") };
|
|
853
|
+
const store = await make(scope);
|
|
854
|
+
const at = "2026-05-24T12:00:00.000Z";
|
|
855
|
+
const make_ = (id: string): LearningLesson =>
|
|
856
|
+
withLessonDefaults(
|
|
857
|
+
{
|
|
858
|
+
id,
|
|
859
|
+
scope,
|
|
860
|
+
kind: "procedure",
|
|
861
|
+
text: `text for ${id}`,
|
|
862
|
+
confidence: 0.5,
|
|
863
|
+
occurrences: 1,
|
|
864
|
+
distinctSessionIds: [],
|
|
865
|
+
evidence: [],
|
|
866
|
+
createdAt: at,
|
|
867
|
+
updatedAt: at,
|
|
868
|
+
},
|
|
869
|
+
at,
|
|
870
|
+
);
|
|
871
|
+
const predecessor = make_("lesson-occupied-pred");
|
|
872
|
+
const bystander = make_("lesson-occupied-bystander");
|
|
873
|
+
await store.upsertLesson(predecessor);
|
|
874
|
+
await store.upsertLesson(bystander);
|
|
875
|
+
|
|
876
|
+
await expect(
|
|
877
|
+
store.supersedeLesson({
|
|
878
|
+
predecessorId: predecessor.id,
|
|
879
|
+
successor: make_(bystander.id),
|
|
880
|
+
now: "2026-05-24T13:00:00.000Z",
|
|
881
|
+
}),
|
|
882
|
+
"a successor id that already belongs to another lesson must be refused",
|
|
883
|
+
).rejects.toThrow(/already belongs to another lesson/);
|
|
884
|
+
|
|
885
|
+
// And nothing may have been written on the way to that refusal.
|
|
886
|
+
expect(await store.getLesson(bystander.id)).toEqual(bystander);
|
|
887
|
+
expect((await store.getLesson(predecessor.id))?.invalidAt).toBeUndefined();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function assertIdempotentResupersede(make: MakeStore) {
|
|
891
|
+
const scope = { agentId: "a1", workspaceId: ws("resupersede") };
|
|
892
|
+
const store = await make(scope);
|
|
893
|
+
const pred: LearningLesson = withLessonDefaults(
|
|
894
|
+
{
|
|
895
|
+
id: "lesson-pred",
|
|
896
|
+
scope,
|
|
897
|
+
kind: "correction",
|
|
898
|
+
text: "first",
|
|
899
|
+
confidence: 0.9,
|
|
900
|
+
occurrences: 1,
|
|
901
|
+
distinctSessionIds: ["s1"],
|
|
902
|
+
evidence: [],
|
|
903
|
+
createdAt: "2026-08-01T00:00:00.000Z",
|
|
904
|
+
updatedAt: "2026-08-01T00:00:00.000Z",
|
|
905
|
+
},
|
|
906
|
+
"2026-08-01T00:00:00.000Z",
|
|
907
|
+
);
|
|
908
|
+
await store.upsertLesson(pred);
|
|
909
|
+
const makeSucc = (id: string, text: string, at: string): LearningLesson =>
|
|
910
|
+
withLessonDefaults(
|
|
911
|
+
{
|
|
912
|
+
id,
|
|
913
|
+
scope,
|
|
914
|
+
kind: "correction",
|
|
915
|
+
text,
|
|
916
|
+
confidence: 0.9,
|
|
917
|
+
occurrences: 1,
|
|
918
|
+
distinctSessionIds: ["s1"],
|
|
919
|
+
evidence: [],
|
|
920
|
+
createdAt: at,
|
|
921
|
+
updatedAt: at,
|
|
922
|
+
},
|
|
923
|
+
at,
|
|
924
|
+
);
|
|
925
|
+
const first = await store.supersedeLesson({
|
|
926
|
+
predecessorId: pred.id,
|
|
927
|
+
successor: makeSucc("lesson-succ-1", "second", "2026-08-02T00:00:00.000Z"),
|
|
928
|
+
now: "2026-08-02T00:00:00.000Z",
|
|
929
|
+
});
|
|
930
|
+
const second = await store.supersedeLesson({
|
|
931
|
+
predecessorId: pred.id,
|
|
932
|
+
successor: makeSucc("lesson-succ-2", "third", "2026-08-03T00:00:00.000Z"),
|
|
933
|
+
now: "2026-08-03T00:00:00.000Z",
|
|
934
|
+
});
|
|
935
|
+
expect(second.id).toBe(first.id);
|
|
936
|
+
const all = await store.listLessons();
|
|
937
|
+
expect(all.filter((lesson) => lesson.supersedesId === pred.id)).toHaveLength(1);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async function assertCandidateRoundTrip(make: MakeStore) {
|
|
941
|
+
const store = await make({ workspaceId: ws("cand"), agentId: "a1" });
|
|
942
|
+
const candidate = withCandidateDefaults(
|
|
943
|
+
{
|
|
944
|
+
sessionId: "sess-1",
|
|
945
|
+
workspaceId: ws("cand"),
|
|
946
|
+
agentId: "a1",
|
|
947
|
+
excerpt: "redacted",
|
|
948
|
+
tags: ["correction"],
|
|
949
|
+
},
|
|
950
|
+
"2026-08-15T00:00:00.000Z",
|
|
951
|
+
);
|
|
952
|
+
await store.upsertCandidate(candidate);
|
|
953
|
+
await store.upsertCandidate({ ...candidate, excerpt: "redacted-2" });
|
|
954
|
+
const got = await store.getCandidate("sess-1");
|
|
955
|
+
expect(got?.excerpt).toBe("redacted-2");
|
|
956
|
+
expect(got?.status).toBe("pending");
|
|
957
|
+
const pending = await store.listPendingCandidates({ tags: ["correction"] });
|
|
958
|
+
expect(pending.map((entry) => entry.sessionId)).toEqual(["sess-1"]);
|
|
959
|
+
await store.markCandidateProcessed("sess-1", {
|
|
960
|
+
status: "processed",
|
|
961
|
+
promotedLessonId: "lesson-x",
|
|
962
|
+
});
|
|
963
|
+
expect((await store.listPendingCandidates()).map((entry) => entry.sessionId)).not.toContain(
|
|
964
|
+
"sess-1",
|
|
965
|
+
);
|
|
966
|
+
expect((await store.getCandidate("sess-1"))?.promotedLessonId).toBe("lesson-x");
|
|
967
|
+
expect(await store.listLessons()).toHaveLength(0);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async function assertGetCandidatesBySessionIdsRoundTrip(make: MakeStore) {
|
|
971
|
+
const scope = { agentId: "a1", workspaceId: ws("batch-candidates") };
|
|
972
|
+
const store = await make(scope);
|
|
973
|
+
const candidate = {
|
|
974
|
+
sessionId: "sess-a",
|
|
975
|
+
excerpt: "redacted",
|
|
976
|
+
tags: ["correction"],
|
|
977
|
+
status: "pending",
|
|
978
|
+
endedAt: "2026-05-24T20:00:00.000Z",
|
|
979
|
+
} as ExtractionCandidate;
|
|
980
|
+
await store.upsertCandidate(candidate);
|
|
981
|
+
await store.upsertCandidate({ ...candidate, sessionId: "sess-b" });
|
|
982
|
+
|
|
983
|
+
const found = await store.getCandidatesBySessionIds(["sess-a", "sess-b", "sess-missing"]);
|
|
984
|
+
expect(
|
|
985
|
+
found.map((entry) => entry.sessionId).sort(),
|
|
986
|
+
"found ids only — missing ids are absent, not an error",
|
|
987
|
+
).toEqual(["sess-a", "sess-b"]);
|
|
988
|
+
expect(await store.getCandidatesBySessionIds([])).toEqual([]);
|
|
989
|
+
|
|
990
|
+
const otherAgent = await make({ agentId: "a2", workspaceId: ws("batch-candidates") });
|
|
991
|
+
expect(
|
|
992
|
+
await otherAgent.getCandidatesBySessionIds(["sess-a"]),
|
|
993
|
+
"agent isolation holds for the batch read in the same workspace",
|
|
994
|
+
).toEqual([]);
|
|
995
|
+
const otherWorkspace = await make({ agentId: "a1", workspaceId: ws("batch-other") });
|
|
996
|
+
expect(
|
|
997
|
+
await otherWorkspace.getCandidatesBySessionIds(["sess-a"]),
|
|
998
|
+
"workspace isolation holds for the batch read with the same agent",
|
|
999
|
+
).toEqual([]);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async function assertSearchLessons(make: MakeStore, backend: Backend) {
|
|
1003
|
+
const store = await make({ workspaceId: ws("srch"), agentId: "a1" });
|
|
1004
|
+
await store.upsertLesson(
|
|
1005
|
+
withLessonDefaults(
|
|
1006
|
+
{
|
|
1007
|
+
id: "lesson-hit",
|
|
1008
|
+
scope: { workspaceId: ws("srch"), agentId: "a1" },
|
|
1009
|
+
kind: "procedure",
|
|
1010
|
+
text: "Always use pnpm-lock.yaml",
|
|
1011
|
+
confidence: 0.9,
|
|
1012
|
+
occurrences: 1,
|
|
1013
|
+
distinctSessionIds: ["s"],
|
|
1014
|
+
evidence: [],
|
|
1015
|
+
createdAt: "2026-08-15T00:00:00.000Z",
|
|
1016
|
+
updatedAt: "2026-08-15T00:00:00.000Z",
|
|
1017
|
+
},
|
|
1018
|
+
"2026-08-15T00:00:00.000Z",
|
|
1019
|
+
),
|
|
1020
|
+
);
|
|
1021
|
+
await store.upsertLesson(
|
|
1022
|
+
withLessonDefaults(
|
|
1023
|
+
{
|
|
1024
|
+
id: "lesson-miss",
|
|
1025
|
+
scope: { workspaceId: ws("srch"), agentId: "a1" },
|
|
1026
|
+
kind: "procedure",
|
|
1027
|
+
text: "Prefer HTTPS",
|
|
1028
|
+
confidence: 0.9,
|
|
1029
|
+
occurrences: 1,
|
|
1030
|
+
distinctSessionIds: ["s"],
|
|
1031
|
+
evidence: [],
|
|
1032
|
+
createdAt: "2026-08-15T00:00:00.000Z",
|
|
1033
|
+
updatedAt: "2026-08-15T00:00:00.000Z",
|
|
1034
|
+
},
|
|
1035
|
+
"2026-08-15T00:00:00.000Z",
|
|
1036
|
+
),
|
|
1037
|
+
);
|
|
1038
|
+
await store.upsertCandidate(
|
|
1039
|
+
withCandidateDefaults(
|
|
1040
|
+
{
|
|
1041
|
+
sessionId: "sess-search",
|
|
1042
|
+
workspaceId: ws("srch"),
|
|
1043
|
+
agentId: "a1",
|
|
1044
|
+
excerpt: "Always use pnpm-lock.yaml",
|
|
1045
|
+
tags: ["untagged"],
|
|
1046
|
+
},
|
|
1047
|
+
"2026-08-15T00:00:00.000Z",
|
|
1048
|
+
),
|
|
1049
|
+
);
|
|
1050
|
+
const hits = await store.searchLessons(
|
|
1051
|
+
"pnpm-lock",
|
|
1052
|
+
{ workspaceId: ws("srch"), agentId: "a1" },
|
|
1053
|
+
5,
|
|
1054
|
+
);
|
|
1055
|
+
expect(hits[0]?.lesson.id).toBe("lesson-hit");
|
|
1056
|
+
expect(hits[0]?.score).toBeGreaterThan(0);
|
|
1057
|
+
expect(hits.map((h) => h.lesson.id)).not.toContain("lesson-miss");
|
|
1058
|
+
expect(hits.map((h) => h.lesson.id)).not.toContain("sess-search");
|
|
1059
|
+
|
|
1060
|
+
await backend.assertSearchIndex(store);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
async function assertFindProposalRejectsCrossScopeAmbiguity(make: MakeStore) {
|
|
1064
|
+
// A proposal id is hash(lessonId + skillName) and a lesson id is hash(text) -- neither carries
|
|
1065
|
+
// the scope. Two workspaces that learn the same text and propose the same skill therefore mint
|
|
1066
|
+
// the SAME id, which the (workspace, agent, type, id) key permits. The cross-scope lookup then
|
|
1067
|
+
// has no correct answer, so it must refuse rather than hand the gateway an arbitrary scope and
|
|
1068
|
+
// apply one owner's decision to the other's proposal.
|
|
1069
|
+
const shared: SkillProposal = {
|
|
1070
|
+
id: "proposal-collides",
|
|
1071
|
+
lessonIds: ["lesson-same-text"],
|
|
1072
|
+
status: "pending",
|
|
1073
|
+
skillName: "same-skill",
|
|
1074
|
+
title: "Collides",
|
|
1075
|
+
body: "# Collides",
|
|
1076
|
+
scannerFindings: [],
|
|
1077
|
+
createdAt: "2026-05-24T20:00:00.000Z",
|
|
1078
|
+
updatedAt: "2026-05-24T20:00:00.000Z",
|
|
1079
|
+
};
|
|
1080
|
+
const left = await make({ agentId: "a1", workspaceId: ws("ambiguous-left") });
|
|
1081
|
+
const right = await make({ agentId: "a2", workspaceId: ws("ambiguous-right") });
|
|
1082
|
+
// upsertProposal, not insertProposalIfAbsent: the duplicate guard is scope-filtered, and the
|
|
1083
|
+
// in-memory Postgres double does not honour that filter, so it would refuse the second write and
|
|
1084
|
+
// never build the two-scope state this asserts on. The collision is what is under test here, not
|
|
1085
|
+
// how it is created.
|
|
1086
|
+
await left.upsertProposal(shared);
|
|
1087
|
+
await right.upsertProposal(shared);
|
|
1088
|
+
|
|
1089
|
+
const crossScope = await make({});
|
|
1090
|
+
await expect(
|
|
1091
|
+
crossScope.findProposalWithScope("proposal-collides"),
|
|
1092
|
+
"an id present in two scopes must be refused, not resolved to an arbitrary one",
|
|
1093
|
+
).rejects.toThrow(/exists in 2 scopes/);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
async function assertListProposalsWithScopeKeepsCrossScopeSameId(make: MakeStore) {
|
|
1097
|
+
const shared: SkillProposal = {
|
|
1098
|
+
id: "proposal-collides-list",
|
|
1099
|
+
lessonIds: ["lesson-same-text"],
|
|
1100
|
+
status: "pending",
|
|
1101
|
+
skillName: "same-skill",
|
|
1102
|
+
title: "Collides",
|
|
1103
|
+
body: "# Collides",
|
|
1104
|
+
scannerFindings: [],
|
|
1105
|
+
createdAt: "2026-05-24T20:00:00.000Z",
|
|
1106
|
+
updatedAt: "2026-05-24T20:00:00.000Z",
|
|
1107
|
+
};
|
|
1108
|
+
const left = await make({ agentId: "a1", workspaceId: ws("list-left") });
|
|
1109
|
+
const right = await make({ agentId: "a2", workspaceId: ws("list-right") });
|
|
1110
|
+
// upsertProposal, not insertProposalIfAbsent — same reason as the ambiguity assert above
|
|
1111
|
+
// (:1138-1141): the duplicate guard is scope-filtered and the in-memory postgres double does
|
|
1112
|
+
// not honour that filter, so it would refuse the second write.
|
|
1113
|
+
await left.upsertProposal(shared);
|
|
1114
|
+
await right.upsertProposal(shared);
|
|
1115
|
+
|
|
1116
|
+
const crossScope = await make({});
|
|
1117
|
+
const matches = (await crossScope.listProposalsWithScope()).filter(
|
|
1118
|
+
(entry) => entry.proposal.id === shared.id,
|
|
1119
|
+
);
|
|
1120
|
+
expect(
|
|
1121
|
+
matches,
|
|
1122
|
+
"both scopes' rows must be listed — fs-jsonl used to collapse them by recordId (finding 17)",
|
|
1123
|
+
).toHaveLength(2);
|
|
1124
|
+
expect(matches.map((entry) => entry.scope)).toEqual(
|
|
1125
|
+
expect.arrayContaining([
|
|
1126
|
+
{ agentId: "a1", workspaceId: ws("list-left") },
|
|
1127
|
+
{ agentId: "a2", workspaceId: ws("list-right") },
|
|
1128
|
+
]),
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
async function assertAlreadyDecidedDecisionRejectsWithCode(make: MakeStore) {
|
|
1133
|
+
const store = await make({ agentId: "a1", workspaceId: ws("decided-code") });
|
|
1134
|
+
const proposal: SkillProposal = {
|
|
1135
|
+
id: "proposal-decided-code",
|
|
1136
|
+
lessonIds: ["lesson-x"],
|
|
1137
|
+
status: "pending",
|
|
1138
|
+
skillName: "decided-code",
|
|
1139
|
+
title: "Decided",
|
|
1140
|
+
body: "# Decided",
|
|
1141
|
+
scannerFindings: [],
|
|
1142
|
+
createdAt: "2026-05-24T20:00:00.000Z",
|
|
1143
|
+
updatedAt: "2026-05-24T20:00:00.000Z",
|
|
1144
|
+
};
|
|
1145
|
+
await store.upsertProposal(proposal);
|
|
1146
|
+
await store.recordProposalDecision({
|
|
1147
|
+
proposalId: proposal.id,
|
|
1148
|
+
status: "applied",
|
|
1149
|
+
actor: "owner",
|
|
1150
|
+
now: "2026-05-24T21:00:00.000Z",
|
|
1151
|
+
});
|
|
1152
|
+
await expect(
|
|
1153
|
+
store.recordProposalDecision({
|
|
1154
|
+
proposalId: proposal.id,
|
|
1155
|
+
status: "rejected",
|
|
1156
|
+
actor: "owner",
|
|
1157
|
+
now: "2026-05-24T22:00:00.000Z",
|
|
1158
|
+
}),
|
|
1159
|
+
"an already-decided refusal must carry a LEARNING_* code on every backend leg",
|
|
1160
|
+
).rejects.toMatchObject({ code: expect.stringMatching(/^LEARNING_/) });
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
async function assertInvalidateLesson(make: MakeStore) {
|
|
1164
|
+
const scope = { agentId: "a1", workspaceId: ws("invalidate") };
|
|
1165
|
+
const store = await make(scope);
|
|
1166
|
+
const now = "2026-05-24T17:00:00.000Z";
|
|
1167
|
+
const lesson = withLessonDefaults(
|
|
1168
|
+
{
|
|
1169
|
+
id: "lesson-invalidate",
|
|
1170
|
+
scope,
|
|
1171
|
+
kind: "procedure" as const,
|
|
1172
|
+
text: "Use pnpm for installs.",
|
|
1173
|
+
confidence: 0.8,
|
|
1174
|
+
occurrences: 1,
|
|
1175
|
+
distinctSessionIds: ["sess-1"],
|
|
1176
|
+
evidence: [],
|
|
1177
|
+
createdAt: now,
|
|
1178
|
+
updatedAt: now,
|
|
1179
|
+
},
|
|
1180
|
+
now,
|
|
1181
|
+
);
|
|
1182
|
+
await store.upsertLesson(lesson);
|
|
1183
|
+
const later = "2026-05-24T18:00:00.000Z";
|
|
1184
|
+
const invalidated = await store.invalidateLesson({
|
|
1185
|
+
id: "lesson-invalidate",
|
|
1186
|
+
now: later,
|
|
1187
|
+
reason: "contradicted",
|
|
1188
|
+
sessionId: "sess-9",
|
|
1189
|
+
});
|
|
1190
|
+
expect(invalidated).toMatchObject({
|
|
1191
|
+
id: "lesson-invalidate",
|
|
1192
|
+
text: "Use pnpm for installs.",
|
|
1193
|
+
invalidAt: later,
|
|
1194
|
+
invalidatedBySessionId: "sess-9",
|
|
1195
|
+
invalidationReason: "contradicted",
|
|
1196
|
+
});
|
|
1197
|
+
// The field round-trips through the store, no successor is minted, and it is inactive.
|
|
1198
|
+
expect(await store.getLesson("lesson-invalidate")).toMatchObject({
|
|
1199
|
+
invalidAt: later,
|
|
1200
|
+
invalidatedBySessionId: "sess-9",
|
|
1201
|
+
});
|
|
1202
|
+
expect(await store.listLessons()).toHaveLength(1);
|
|
1203
|
+
expect(await store.listActiveLessons(later)).toEqual([]);
|
|
1204
|
+
// Idempotent: a second invalidation keeps the first invalidAt.
|
|
1205
|
+
const again = await store.invalidateLesson({
|
|
1206
|
+
id: "lesson-invalidate",
|
|
1207
|
+
now: "2026-05-25T00:00:00.000Z",
|
|
1208
|
+
});
|
|
1209
|
+
expect(again.invalidAt).toBe(later);
|
|
1210
|
+
await expect(store.invalidateLesson({ id: "lesson-missing", now: later })).rejects.toThrow(
|
|
1211
|
+
/Lesson not found/,
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// VAL-PROD-022: invalidateLesson leaves a lesson retired with no successor. The public
|
|
1216
|
+
// (non-exact) supersede must refuse it and write nothing: no restamped invalidAt, no active
|
|
1217
|
+
// successor for a retired lesson.
|
|
1218
|
+
async function assertSupersedeRefusesInvalidatedWithoutSuccessor(
|
|
1219
|
+
make: MakeStore,
|
|
1220
|
+
backend: Backend,
|
|
1221
|
+
) {
|
|
1222
|
+
const scope = { agentId: "a1", workspaceId: ws("supersede-invalidated") };
|
|
1223
|
+
const store = await make(scope);
|
|
1224
|
+
const created = "2026-08-01T00:00:00.000Z";
|
|
1225
|
+
const invalidatedAt = "2026-08-02T00:00:00.000Z";
|
|
1226
|
+
await store.upsertLesson(
|
|
1227
|
+
withLessonDefaults(
|
|
1228
|
+
{
|
|
1229
|
+
id: "lesson-retired",
|
|
1230
|
+
scope,
|
|
1231
|
+
kind: "correction",
|
|
1232
|
+
text: "retired text",
|
|
1233
|
+
confidence: 0.9,
|
|
1234
|
+
occurrences: 1,
|
|
1235
|
+
distinctSessionIds: ["s1"],
|
|
1236
|
+
evidence: [],
|
|
1237
|
+
createdAt: created,
|
|
1238
|
+
updatedAt: created,
|
|
1239
|
+
},
|
|
1240
|
+
created,
|
|
1241
|
+
),
|
|
1242
|
+
);
|
|
1243
|
+
await store.invalidateLesson({
|
|
1244
|
+
id: "lesson-retired",
|
|
1245
|
+
now: invalidatedAt,
|
|
1246
|
+
reason: "contradicted",
|
|
1247
|
+
sessionId: "sess-inv",
|
|
1248
|
+
});
|
|
1249
|
+
const before = await backend.snapshot();
|
|
1250
|
+
const later = "2026-08-03T00:00:00.000Z";
|
|
1251
|
+
const error = await store
|
|
1252
|
+
.supersedeLesson({
|
|
1253
|
+
predecessorId: "lesson-retired",
|
|
1254
|
+
successor: withLessonDefaults(
|
|
1255
|
+
{
|
|
1256
|
+
id: "lesson-resurrected",
|
|
1257
|
+
scope,
|
|
1258
|
+
kind: "correction",
|
|
1259
|
+
text: "resurrected text",
|
|
1260
|
+
confidence: 0.9,
|
|
1261
|
+
occurrences: 1,
|
|
1262
|
+
distinctSessionIds: ["s1"],
|
|
1263
|
+
evidence: [],
|
|
1264
|
+
createdAt: later,
|
|
1265
|
+
updatedAt: later,
|
|
1266
|
+
},
|
|
1267
|
+
later,
|
|
1268
|
+
),
|
|
1269
|
+
now: later,
|
|
1270
|
+
})
|
|
1271
|
+
.then(
|
|
1272
|
+
() => undefined,
|
|
1273
|
+
(err: unknown) => err,
|
|
1274
|
+
);
|
|
1275
|
+
expect(error).toBeInstanceOf(Error);
|
|
1276
|
+
expect(isLearningDomainError(error)).toBe(true);
|
|
1277
|
+
expect((error as Error).message).toMatch(/invalidated and has no successor/);
|
|
1278
|
+
expect(await backend.snapshot()).toBe(before);
|
|
1279
|
+
expect(await store.getLesson("lesson-retired")).toMatchObject({
|
|
1280
|
+
invalidAt: invalidatedAt,
|
|
1281
|
+
invalidatedBySessionId: "sess-inv",
|
|
1282
|
+
invalidationReason: "contradicted",
|
|
1283
|
+
});
|
|
1284
|
+
expect(await store.getLesson("lesson-resurrected")).toBeUndefined();
|
|
1285
|
+
const all = await store.listLessons();
|
|
1286
|
+
expect(all.filter((lesson) => lesson.supersedesId === "lesson-retired")).toEqual([]);
|
|
1287
|
+
expect(
|
|
1288
|
+
(await store.listActiveLessons(later)).filter((l) => l.supersedesId === "lesson-retired"),
|
|
1289
|
+
).toEqual([]);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
async function assertDescribeBackend(make: MakeStore, expected: Backend["name"]) {
|
|
1293
|
+
const store = await make({ agentId: "a9", workspaceId: ws("describe") });
|
|
1294
|
+
const desc = store.describeBackend();
|
|
1295
|
+
|
|
1296
|
+
expect(desc.backend, `${expected} store must report its own backend name`).toBe(expected);
|
|
1297
|
+
expect(desc.capability, `${expected} store must report its documented capability`).toBe(
|
|
1298
|
+
EXPECTED_CAPABILITY[expected],
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// --- Backend tests: registered once per leg by each conformance test file ---
|
|
1303
|
+
|
|
1304
|
+
export function registerLessonStoreConformance(backend: Backend): void {
|
|
1305
|
+
describe(`LessonStore conformance — ${backend.label}`, () => {
|
|
1306
|
+
it("round-trips a lesson with every lifecycle field", async () => {
|
|
1307
|
+
await assertRoundTripsLesson(backend.make);
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
it("upsert is idempotent by id within a scope", async () => {
|
|
1311
|
+
await assertUpsertIsIdempotent(backend.make);
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
it("scopes reads — a lesson in w1/a1 is invisible to w2/a2", async () => {
|
|
1315
|
+
await assertScopeIsolation(backend.make);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
it("isolates on workspace and on agent independently", async () => {
|
|
1319
|
+
await assertScopeIsolationPerDimension(backend.make);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
it("an empty scope reads across all scopes", async () => {
|
|
1323
|
+
await assertEmptyScopeReadsAcrossScopes(backend.make);
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
it("supersedeLesson retains the predecessor with invalid_at and links the successor", async () => {
|
|
1327
|
+
await assertSupersedeRetainsPredecessor(backend.make);
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
it("updating an earlier lesson does not reorder the list", async () => {
|
|
1331
|
+
await assertUpdateDoesNotReorder(backend.make);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
it("supersedeLesson refuses a successor whose id equals the predecessor id", async () => {
|
|
1335
|
+
await assertSupersedeRejectsSelfReference(backend.make);
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
it("invalidateLesson ends a lesson without a successor and records invalidatedBySessionId", async () => {
|
|
1339
|
+
await assertInvalidateLesson(backend.make);
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
it("listActiveLessons excludes invalidated records; getLesson still returns them", async () => {
|
|
1343
|
+
await assertActiveListExcludesInvalidated(backend.make);
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
it("getLessonChain walks the chain newest-first and rejects a cycle", async () => {
|
|
1347
|
+
await assertChainWalksNewestFirstAndRejectsCycle(backend.make);
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
it("insertLessonIfAbsent never overwrites an existing row", async () => {
|
|
1351
|
+
await assertInsertLessonIfAbsentNeverOverwrites(backend.make);
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
it("mergeLesson accumulates occurrences and evidence", async () => {
|
|
1355
|
+
await assertMergeAccumulatesOccurrencesAndEvidence(backend.make);
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
it("proposal + audit round-trip", async () => {
|
|
1359
|
+
await assertProposalAndAuditRoundTrip(backend.make);
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
it("findProposalWithScope returns a proposal's own scope even when queried from an empty scope", async () => {
|
|
1363
|
+
await assertFindProposalWithScope(backend.make);
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
it("supersedeLesson REFUSES a successor id that already belongs to another lesson", async () => {
|
|
1367
|
+
await assertSupersedeRejectsOccupiedSuccessorId(backend.make);
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
it("supersedeLesson refuses an invalidated lesson with no successor and writes nothing (VAL-PROD-022)", async () => {
|
|
1371
|
+
await assertSupersedeRefusesInvalidatedWithoutSuccessor(backend.make, backend);
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
it("re-superseding an invalidated lesson returns the existing successor", async () => {
|
|
1375
|
+
await assertIdempotentResupersede(backend.make);
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
it("findProposalWithScope REFUSES an id that exists in two scopes", async () => {
|
|
1379
|
+
await assertFindProposalRejectsCrossScopeAmbiguity(backend.make);
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
it("listProposalsWithScope keeps both scopes' rows for a cross-scope same-id proposal", async () => {
|
|
1383
|
+
await assertListProposalsWithScopeKeepsCrossScopeSameId(backend.make);
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
it("recordProposalDecision on an already-decided proposal rejects with a LEARNING_* code", async () => {
|
|
1387
|
+
await assertAlreadyDecidedDecisionRejectsWithCode(backend.make);
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
it("describeBackend reports the expected backend and capability", async () => {
|
|
1391
|
+
await assertDescribeBackend(backend.make, backend.name);
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
it("round-trips a candidate and keeps it out of listLessons", async () => {
|
|
1395
|
+
await assertCandidateRoundTrip(backend.make);
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
it("batches candidate lookups by session id set", async () => {
|
|
1399
|
+
await assertGetCandidatesBySessionIdsRoundTrip(backend.make);
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
it("searchLessons ranks the distinctive token and skips candidates (TS12)", async () => {
|
|
1403
|
+
await assertSearchLessons(backend.make, backend);
|
|
1404
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
}
|