wednesdayai 0.4.12-dev.716 → 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-runner-Bvj00mHU.js → agent-turn-runner-BsdyAURQ.js} +3 -3
- package/dist/{audio-preflight-yuM9Q3qh.js → audio-preflight-ifCgpG3c.js} +5 -5
- package/dist/{audio-transcription-runner-CgPA8_eG.js → audio-transcription-runner-ipSQoPp8.js} +1 -1
- package/dist/build-info.json +3 -3
- package/dist/bundled/boot-md/handler.js +12 -12
- package/dist/bundled/session-journal/handler.js +12 -12
- package/dist/bundled/session-memory/handler.js +12 -12
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/{chrome-BkJgMZjT.js → chrome-DTJprCGv.js} +8 -8
- package/dist/cli/daemon-cli.js +1 -1
- package/dist/{command-queue-DucYxKKF.js → command-queue-BTAxEdto.js} +1 -1
- package/dist/{command-registry-NOHWjbDr.js → command-registry-C7nkhshN.js} +7 -7
- package/dist/{completion-cli-BfKoz1jW.js → completion-cli-CjPs9az4.js} +2 -2
- package/dist/{completion-cli-BQ5pz74O.js → completion-cli-DozvikmZ.js} +1 -1
- package/dist/{config-cli-BGFZu6JD.js → config-cli-Dagtz6FG.js} +1 -1
- package/dist/{config-cli-cFzUpYdg.js → config-cli-Dv1Dad57.js} +1 -1
- package/dist/config.js +9 -9
- package/dist/{configure-DYAZJ3GF.js → configure-BbKf530X.js} +2 -2
- package/dist/{configure-DrrwPxlu.js → configure-CBJSHejz.js} +2 -2
- 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-fwGuXC0n.js → deliver-CVWbwPkb.js} +1 -1
- package/dist/{diagnostic-BB6k_yiT.js → diagnostic-DI-K8155.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/{doctor-completion-CtR8yetH.js → doctor-completion-CJYV2FQ5.js} +1 -1
- package/dist/{doctor-completion-B94pFOFt.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/{gateway-cli-DI7_gxH-.js → gateway-cli-Cyivh-5T.js} +10 -10
- package/dist/{gateway-cli-C_veSlCx.js → gateway-cli-DtFDQj5d.js} +10 -10
- package/dist/index.js +1 -1
- package/dist/{inspect-DVHAPbcZ.js → inspect-BXlZnWkg.js} +1 -1
- package/dist/{inspect-gvjq38z1.js → inspect-LfUprDjx.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 +12 -12
- package/dist/{node-cli-m0j_YD0V.js → node-cli-Csimlxec.js} +5 -5
- package/dist/{node-cli-yC-cX-mJ.js → node-cli-dh4qWKpT.js} +5 -5
- 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/{onboard-D8MZoOum.js → onboard-DuNuHUvh.js} +2 -2
- package/dist/{onboard-B9-DxI_m.js → onboard-_pVU_O8s.js} +2 -2
- package/dist/{onboarding-0WSY8o8f.js → onboarding-BBb8ZbfX.js} +1 -1
- package/dist/{onboarding-BfpXrIw7.js → onboarding-BcI8N4WK.js} +1 -1
- package/dist/{onboarding.finalize-QITC8MsS.js → onboarding.finalize-BXkz7oPa.js} +7 -7
- package/dist/{onboarding.finalize-BnIw1M2b.js → onboarding.finalize-D9fyrhAS.js} +6 -6
- package/dist/{pi-embedded-BBId5n_k.js → pi-embedded-Qyba-Rws.js} +28 -28
- package/dist/{pi-embedded-helpers-DVvJRC5S.js → pi-embedded-helpers-5JApWu7A.js} +3 -3
- package/dist/{pi-embedded-utils-BoaQCwP7.js → pi-embedded-utils-kHMmP2Xh.js} +1 -1
- package/dist/plugin-sdk/{agent-scope-BQYVQxfQ.js → agent-scope-DU8uJiiv.js} +2 -2
- package/dist/plugin-sdk/agent-turn-execute-DIrSWfHZ.js +61 -0
- package/dist/plugin-sdk/{agent-turn-runner-DL5jOnXC.js → agent-turn-runner-CmDXH4Z8.js} +6 -6
- package/dist/plugin-sdk/{api-key-rotation-BW3rq9Tg.js → api-key-rotation-Oe0hkmVQ.js} +2 -2
- 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-transcription-runner-CGgNZVZv.js → audio-transcription-runner-CUsZFiEA.js} +11 -11
- package/dist/plugin-sdk/{auth-profiles-CIKTWees.js → auth-profiles-OgKl-vr-.js} +5 -5
- package/dist/plugin-sdk/{channel-activity-Ck83U_es.js → channel-activity-OIauZrkN.js} +3 -3
- package/dist/plugin-sdk/{chrome-CT_6RvJg.js → chrome-VZ0Zuu2w.js} +4 -4
- package/dist/plugin-sdk/{chunk-D6yDhUpF.js → chunk-BWz-lIzS.js} +4 -4
- package/dist/plugin-sdk/{command-queue-DH8k55DC.js → command-queue-D_UFlrwC.js} +2 -2
- package/dist/plugin-sdk/{commands-registry-DuwlD_aI.js → commands-registry-XW2hmbAE.js} +3 -3
- package/dist/plugin-sdk/{deliver-_UyigkEK.js → deliver-B7W4NNsR.js} +11 -11
- package/dist/plugin-sdk/deliver-CJP9TznW.js +32 -0
- package/dist/plugin-sdk/{diagnostic-Dk57KVjE.js → diagnostic-BRUM6uNI.js} +7 -7
- 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-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-BQ5cHy9G.js → fs-safe-ClebEeLz.js} +3 -3
- package/dist/plugin-sdk/{image-ops-0TjWNKUx.js → image-ops-Bm03YsK2.js} +1 -1
- package/dist/plugin-sdk/{inbound-context-Bfr5DkdO.js → inbound-context-Cmgtf5my.js} +1 -1
- package/dist/plugin-sdk/{ir-CbL0sVL2.js → ir-1CthVxHv.js} +5 -5
- package/dist/plugin-sdk/{logger-D5s3kiib.js → logger-BkzoyfHW.js} +1 -1
- 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/{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-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-utils-AFQ-oCFM.js → pi-embedded-utils-BoSGTbYO.js} +3 -3
- package/dist/plugin-sdk/{pi-model-discovery-C37z3zkx.js → pi-model-discovery-DF6l-F8s.js} +2 -2
- package/dist/plugin-sdk/provider-dispatcher-CAfyX0H-.js +61 -0
- 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/provider-media-understanding.js +7 -7
- package/dist/plugin-sdk/{proxy-CjNR_U3u.js → proxy-CXI1ycJj.js} +2 -2
- package/dist/plugin-sdk/{proxy-env-ubutfW07.js → proxy-env-B3rQCyUO.js} +2 -2
- package/dist/plugin-sdk/{proxy-fetch-Cv4b428x.js → proxy-fetch-CNBkczcW.js} +1 -1
- 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/{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/{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-BFjGWAPs.js +25 -0
- package/dist/plugin-sdk/{replies-CjeAGcJx.js → replies-CS2GXrd-.js} +4 -4
- 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/{resolve-route-BLBv69wV.js → resolve-route-tqQv32nr.js} +5 -5
- package/dist/plugin-sdk/runs-CPA1-H-2.js +25 -0
- package/dist/plugin-sdk/{runs-CPWQbljV.js → runs-H2FY7UEk.js} +8 -8
- 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-CzxwgKvR.js +27 -0
- 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-DyYaWr3E.js +24 -0
- package/dist/plugin-sdk/{session-meta-D3W538Wj.js → session-meta-oR-S9C5J.js} +3 -3
- package/dist/plugin-sdk/{skill-commands-ByVQek_m.js → skill-commands-Bh43SlDc.js} +6 -6
- package/dist/plugin-sdk/{skills-BGgry_YW.js → skills-CatBv8W4.js} +6 -6
- package/dist/plugin-sdk/{store-C6ZLKWUS.js → store-6JKvp2Zu.js} +2 -2
- package/dist/plugin-sdk/subagent-registry-DwnBzoKn.js +61 -0
- 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/testing.js +55 -55
- package/dist/plugin-sdk/{tokens-O0pGqOuf.js → tokens-Cioh8xR5.js} +1 -1
- package/dist/plugin-sdk/{tool-loop-detection-BG8uP1aD.js → tool-loop-detection-BN0lIxLx.js} +1 -1
- package/dist/plugin-sdk/{tools-PVG4MyCb.js → tools-4rcPGSNh.js} +93 -93
- package/dist/plugin-sdk/{user-run-recording-iS-qLIN9.js → user-run-recording-DlpzQk0X.js} +3 -3
- package/dist/{program-CLZlTcBu.js → program-CB5O-AyY.js} +2 -2
- package/dist/{program-context-DN3ucEgK.js → program-context-DxwZAeg4.js} +12 -12
- package/dist/{prompt-select-styled-BNUoeaMg.js → prompt-select-styled-1UYyuvWX.js} +7 -7
- package/dist/{prompt-select-styled-CtLCU2VG.js → prompt-select-styled-CfTLn2TL.js} +7 -7
- package/dist/{provider-image-Bi2L9LWh.js → provider-image-Cb4z6zUu.js} +1 -1
- package/dist/{pw-ai-pYtWxKPR.js → pw-ai-DziGw6Jy.js} +1 -1
- package/dist/{register.configure-CiYkPB_K.js → register.configure-DUg0MNnJ.js} +4 -4
- package/dist/{register.configure-Ceru0oh0.js → register.configure-PJZ3F6Y4.js} +4 -4
- package/dist/{register.maintenance-D-kzdtog.js → register.maintenance-9XyX4HYf.js} +10 -10
- package/dist/{register.maintenance-BCSq69vI.js → register.maintenance-BBM-rluy.js} +9 -9
- package/dist/{register.onboard-BPko9BHz.js → register.onboard-CEzQdSxa.js} +2 -2
- package/dist/{register.onboard-BHBpxcRv.js → register.onboard-id-BqwSw.js} +2 -2
- package/dist/{register.setup-Bjx48C4T.js → register.setup-BKBe-Sjp.js} +2 -2
- package/dist/{register.setup-BE-sR-Dc.js → register.setup-BiOdr528.js} +2 -2
- package/dist/{register.status-health-sessions-BczZXjHB.js → register.status-health-sessions-BVIHCaPP.js} +5 -5
- package/dist/{register.status-health-sessions-CQQWiYcu.js → register.status-health-sessions-CeRiMlKm.js} +5 -5
- package/dist/{register.subclis-Bn9Re4fV.js → register.subclis-IXr-WEhq.js} +6 -6
- package/dist/{run-main-Cfccgza4.js → run-main-CsSM1uCJ.js} +6 -6
- package/dist/{runs-BauIdJqy.js → runs-ucQd8Fc-.js} +3 -3
- package/dist/{send-Dd2SE0h0.js → send-TscXeVY8.js} +1 -1
- package/dist/{service-Hy5BSgmp.js → service-C_LTJsJU.js} +1 -1
- package/dist/{service-RnhuB5cG.js → service-Dkcqg-rd.js} +1 -1
- 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-Dh0qVJb_.js → status-Czr0TmiN.js} +3 -3
- package/dist/{status-PwhjcAYw.js → status-DJsk1JjI.js} +3 -3
- 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/{update-cli-f5iTpbNn.js → update-cli-B4_xRNut.js} +11 -11
- package/dist/{update-cli-BSxfgTyN.js → update-cli-DDmfG1gm.js} +12 -12
- 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-Dd5AJiKY.js +0 -61
- package/dist/plugin-sdk/deliver-DuDcszu1.js +0 -32
- package/dist/plugin-sdk/manager-Bqpqu2GS.js +0 -16
- package/dist/plugin-sdk/provider-dispatcher-C4-urx2x.js +0 -61
- package/dist/plugin-sdk/provider-image-Bumd1GBG.js +0 -22
- package/dist/plugin-sdk/replies-0gHRQjbA.js +0 -25
- package/dist/plugin-sdk/runs-B3DB63tB.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-NUHfC-G_.js +0 -25
- package/dist/plugin-sdk/send-Tm5-7bLz.js +0 -24
- package/dist/plugin-sdk/subagent-registry-B0gD-Lof.js +0 -61
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { readRankingPool, RANKING_SCAN_MAX_ROWS } from "./consolidation-judge.js";
|
|
5
|
+
import { withCandidateDefaults } from "./extraction-candidate.js";
|
|
6
|
+
import { createLearningOverview } from "./gateway-methods.js";
|
|
7
|
+
import { withLessonDefaults } from "./lesson-record.js";
|
|
8
|
+
import type { PgClientLike, PgPoolLike } from "./lesson-store-postgres.js";
|
|
9
|
+
import type { SqliteDatabaseLike } from "./lesson-store-sqlite.js";
|
|
10
|
+
import {
|
|
11
|
+
LESSON_EXCERPT_MAX_BYTES,
|
|
12
|
+
LESSON_PAGE_MAX_BYTES,
|
|
13
|
+
type ExtractionCandidate,
|
|
14
|
+
type LessonStore,
|
|
15
|
+
type SkillProposal,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
|
|
18
|
+
// Spec "Bounded store reads, scans, and success evidence": verification asserts rows AND bytes
|
|
19
|
+
// read, not just result length. Every SQL leg runs through a meter that counts the rows the
|
|
20
|
+
// database handed back and the payload bytes among them, so a materialize-then-filter read (the
|
|
21
|
+
// pre-change readPayloads path) fails here even when its result is correct.
|
|
22
|
+
|
|
23
|
+
type Meter = { rows: number; payloadBytes: number; excerptBytes: number };
|
|
24
|
+
|
|
25
|
+
function newMeter(): Meter {
|
|
26
|
+
return { rows: 0, payloadBytes: 0, excerptBytes: 0 };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// One wrapped handle per test (the stores memoise schema init on the handle), metering into
|
|
30
|
+
// whichever meter the last make() installed.
|
|
31
|
+
let activeMeter: Meter = newMeter();
|
|
32
|
+
|
|
33
|
+
function countRows(rows: unknown[]): void {
|
|
34
|
+
const meter = activeMeter;
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
meter.rows += 1;
|
|
37
|
+
for (const [column, value] of Object.entries(row as Record<string, unknown>)) {
|
|
38
|
+
if (column === "payload_json") {
|
|
39
|
+
meter.payloadBytes += Buffer.byteLength(
|
|
40
|
+
typeof value === "string" ? value : JSON.stringify(value),
|
|
41
|
+
);
|
|
42
|
+
} else if (column === "excerpt" && value instanceof Uint8Array) {
|
|
43
|
+
meter.excerptBytes += value.length;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function meteredSqlite(db: DatabaseSync): SqliteDatabaseLike {
|
|
50
|
+
return {
|
|
51
|
+
exec: (sql) => db.exec(sql),
|
|
52
|
+
prepare(sql) {
|
|
53
|
+
const statement = db.prepare(sql);
|
|
54
|
+
return {
|
|
55
|
+
run: (...params) => statement.run(...(params as never[])),
|
|
56
|
+
get: (...params) => {
|
|
57
|
+
const row = statement.get(...(params as never[]));
|
|
58
|
+
countRows(row === undefined ? [] : [row]);
|
|
59
|
+
return row;
|
|
60
|
+
},
|
|
61
|
+
all: (...params) => {
|
|
62
|
+
const rows = statement.all(...(params as never[]));
|
|
63
|
+
countRows(rows);
|
|
64
|
+
return rows;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function meteredPool(pool: PgPoolLike): PgPoolLike {
|
|
72
|
+
const wrap = async (result: Promise<{ rows: Record<string, unknown>[] }>) => {
|
|
73
|
+
const settled = await result;
|
|
74
|
+
countRows(settled.rows);
|
|
75
|
+
return settled;
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
query: (sql, values) => wrap(pool.query(sql, values)),
|
|
79
|
+
async connect(): Promise<PgClientLike> {
|
|
80
|
+
const client = await pool.connect();
|
|
81
|
+
return {
|
|
82
|
+
query: (sql, values) => wrap(client.query(sql, values)),
|
|
83
|
+
release: () => client.release(),
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const RUN_TOKEN = crypto.randomUUID().replaceAll("-", "");
|
|
90
|
+
export const TEST_SCHEMA = `plugin_learning_core_bounded_${RUN_TOKEN}`;
|
|
91
|
+
export const NOW = "2026-06-01T00:00:00.000Z";
|
|
92
|
+
export const PAST = "2026-05-01T00:00:00.000Z";
|
|
93
|
+
const FUTURE = "2027-01-01T00:00:00.000Z";
|
|
94
|
+
|
|
95
|
+
export type SqlLeg = {
|
|
96
|
+
label: string;
|
|
97
|
+
/** Fixture sizes; the live leg is smaller only to keep a shared server fast. */
|
|
98
|
+
scale: number;
|
|
99
|
+
make: (meter: Meter) => LessonStore;
|
|
100
|
+
/** A store scoped to one row's own scope, over the same metered handle `make` reads through. */
|
|
101
|
+
writer: (scope: { workspaceId: string; agentId: string }) => LessonStore;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export function metering<T>(meter: Meter, value: T): T {
|
|
105
|
+
activeMeter = meter;
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function iso(ms: number): string {
|
|
110
|
+
return new Date(Date.parse(PAST) + ms).toISOString();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function candidate(
|
|
114
|
+
index: number,
|
|
115
|
+
status: ExtractionCandidate["status"],
|
|
116
|
+
): ExtractionCandidate {
|
|
117
|
+
return withCandidateDefaults(
|
|
118
|
+
{
|
|
119
|
+
sessionId: `session-${String(index).padStart(6, "0")}`,
|
|
120
|
+
agentId: `agent-${index % 3}`,
|
|
121
|
+
workspaceId: `ws-${index % 2}`,
|
|
122
|
+
excerpt: `candidate ${index} ${"x".repeat(900)}`,
|
|
123
|
+
tags: index % 2 === 0 ? ["correction"] : ["failure"],
|
|
124
|
+
status,
|
|
125
|
+
},
|
|
126
|
+
iso(index * 1000),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function lesson(
|
|
131
|
+
index: number,
|
|
132
|
+
text: string,
|
|
133
|
+
opts?: { invalidAt?: string; validFrom?: string },
|
|
134
|
+
) {
|
|
135
|
+
const at = iso(index * 1000);
|
|
136
|
+
return withLessonDefaults(
|
|
137
|
+
{
|
|
138
|
+
id: `lesson-${String(index).padStart(6, "0")}`,
|
|
139
|
+
scope: { agentId: `agent-${index % 3}`, workspaceId: `ws-${index % 2}` },
|
|
140
|
+
kind: "procedure" as const,
|
|
141
|
+
text,
|
|
142
|
+
confidence: 0.9,
|
|
143
|
+
occurrences: 1,
|
|
144
|
+
distinctSessionIds: [`s-${index}`],
|
|
145
|
+
evidence: [],
|
|
146
|
+
createdAt: at,
|
|
147
|
+
updatedAt: at,
|
|
148
|
+
...(opts?.validFrom ? { validFrom: opts.validFrom } : {}),
|
|
149
|
+
...(opts?.invalidAt ? { invalidAt: opts.invalidAt } : {}),
|
|
150
|
+
},
|
|
151
|
+
at,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function proposal(index: number, status: SkillProposal["status"]): SkillProposal {
|
|
156
|
+
return {
|
|
157
|
+
id: `proposal-${index}`,
|
|
158
|
+
lessonIds: [`lesson-${String(index).padStart(6, "0")}`],
|
|
159
|
+
status,
|
|
160
|
+
skillName: `skill-${index}`,
|
|
161
|
+
title: `Proposal ${index}`,
|
|
162
|
+
body: "b".repeat(4000),
|
|
163
|
+
scannerFindings: [],
|
|
164
|
+
createdAt: iso(index),
|
|
165
|
+
updatedAt: iso(index),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Mixed 1-, 2- and 3-byte characters, so a byte cut can land inside a character.
|
|
170
|
+
const LONG_TEXT = `${"é".repeat(3000)}${"界".repeat(2000)} tail`;
|
|
171
|
+
|
|
172
|
+
async function seedScoped(
|
|
173
|
+
writer: (scope: { workspaceId: string; agentId: string }) => LessonStore,
|
|
174
|
+
items: Array<{
|
|
175
|
+
scope: { workspaceId: string; agentId: string };
|
|
176
|
+
write: (store: LessonStore) => Promise<void>;
|
|
177
|
+
}>,
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
for (const item of items) {
|
|
180
|
+
await item.write(writer(item.scope));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function registerBoundedSqlLegSuite(leg: SqlLeg): void {
|
|
185
|
+
describe(`bounded SQL store reads — ${leg.label}`, () => {
|
|
186
|
+
// Writes go through a store scoped to the row's own scope (as production capture does), reads
|
|
187
|
+
// through the unscoped store the gateway and the passes use.
|
|
188
|
+
const scopedWriter = (meter: Meter) => (scope: { workspaceId: string; agentId: string }) => {
|
|
189
|
+
activeMeter = meter;
|
|
190
|
+
return leg.writer(scope);
|
|
191
|
+
};
|
|
192
|
+
const scopeOf = (index: number) => ({
|
|
193
|
+
agentId: `agent-${index % 3}`,
|
|
194
|
+
workspaceId: `ws-${index % 2}`,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("pending candidates: rows and payload bytes read are the pending set only, keyset-paged", async () => {
|
|
198
|
+
const total = Math.round(2000 * leg.scale);
|
|
199
|
+
const pendingEvery = 100;
|
|
200
|
+
const seedMeter = newMeter();
|
|
201
|
+
const writer = scopedWriter(seedMeter);
|
|
202
|
+
const all: ExtractionCandidate[] = [];
|
|
203
|
+
for (let index = 0; index < total; index += 1) {
|
|
204
|
+
const entry = candidate(index, index % pendingEvery === 0 ? "pending" : "processed");
|
|
205
|
+
all.push(entry);
|
|
206
|
+
await writer(scopeOf(index)).upsertCandidate(entry);
|
|
207
|
+
}
|
|
208
|
+
const expected = all.filter((entry) => entry.status === "pending");
|
|
209
|
+
const pendingBytes = expected.reduce(
|
|
210
|
+
(sum, entry) => sum + Buffer.byteLength(JSON.stringify(entry)),
|
|
211
|
+
0,
|
|
212
|
+
);
|
|
213
|
+
const allBytes = all.reduce(
|
|
214
|
+
(sum, entry) => sum + Buffer.byteLength(JSON.stringify(entry)),
|
|
215
|
+
0,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const meter = newMeter();
|
|
219
|
+
const store = leg.make(meter);
|
|
220
|
+
const pending = await store.listPendingCandidates();
|
|
221
|
+
expect(pending.map((entry) => entry.sessionId)).toEqual(
|
|
222
|
+
expected.map((entry) => entry.sessionId),
|
|
223
|
+
);
|
|
224
|
+
expect(meter.rows, "rows read must be the pending rows, not every candidate").toBe(
|
|
225
|
+
expected.length,
|
|
226
|
+
);
|
|
227
|
+
// jsonb re-serialization may reorder keys but never grows a row by more than its key quotes.
|
|
228
|
+
expect(meter.payloadBytes).toBeLessThanOrEqual(pendingBytes * 1.1);
|
|
229
|
+
expect(meter.payloadBytes).toBeLessThan(allBytes / 20);
|
|
230
|
+
|
|
231
|
+
// Keyset pages: LIMIT applies in SQL, pages chain without gaps or repeats, tags filter.
|
|
232
|
+
const pageMeter = newMeter();
|
|
233
|
+
const paged = leg.make(pageMeter);
|
|
234
|
+
const first = await paged.listPendingCandidatesPage({ limit: 7 });
|
|
235
|
+
expect(pageMeter.rows).toBe(7);
|
|
236
|
+
const seen = [...first.candidates];
|
|
237
|
+
let cursor = first.nextCursor;
|
|
238
|
+
while (cursor) {
|
|
239
|
+
const page = await paged.listPendingCandidatesPage({ after: cursor, limit: 7 });
|
|
240
|
+
seen.push(...page.candidates);
|
|
241
|
+
cursor = page.nextCursor;
|
|
242
|
+
}
|
|
243
|
+
expect(seen.map((entry) => entry.sessionId)).toEqual(
|
|
244
|
+
expected.map((entry) => entry.sessionId),
|
|
245
|
+
);
|
|
246
|
+
const tagged = await paged.listPendingCandidates({ tags: ["failure"] });
|
|
247
|
+
expect(tagged.map((entry) => entry.sessionId)).toEqual(
|
|
248
|
+
expected.filter((entry) => entry.tags.includes("failure")).map((entry) => entry.sessionId),
|
|
249
|
+
);
|
|
250
|
+
const limited = await paged.listPendingCandidates({ limit: 3 });
|
|
251
|
+
expect(limited.map((entry) => entry.sessionId)).toEqual(
|
|
252
|
+
expected.slice(0, 3).map((entry) => entry.sessionId),
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("similar-lesson ranking reads { id, excerpt } pages: no payload bytes, 2 KiB rows, 256 KiB pages", async () => {
|
|
257
|
+
const activeCount = Math.round(450 * leg.scale);
|
|
258
|
+
const seedMeter = newMeter();
|
|
259
|
+
const writer = scopedWriter(seedMeter);
|
|
260
|
+
const activeIds: string[] = [];
|
|
261
|
+
for (let index = 0; index < activeCount; index += 1) {
|
|
262
|
+
const entry = lesson(index, index % 5 === 0 ? `short ${index}` : `${LONG_TEXT} ${index}`);
|
|
263
|
+
activeIds.push(entry.id);
|
|
264
|
+
await writer(scopeOf(index)).upsertLesson(entry);
|
|
265
|
+
}
|
|
266
|
+
// Inactive rows the SQL predicate must drop: invalidated, and not yet valid.
|
|
267
|
+
for (let index = activeCount; index < activeCount + 40; index += 1) {
|
|
268
|
+
await writer(scopeOf(index)).upsertLesson(
|
|
269
|
+
lesson(index, LONG_TEXT, index % 2 === 0 ? { invalidAt: PAST } : { validFrom: FUTURE }),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const meter = newMeter();
|
|
274
|
+
const store = leg.make(meter);
|
|
275
|
+
const first = await store.listActiveLessonsPage({ now: NOW, limit: 200 });
|
|
276
|
+
expect(meter.payloadBytes, "no payload_json may leave the database").toBe(0);
|
|
277
|
+
expect(first.lessons.length).toBeLessThanOrEqual(200);
|
|
278
|
+
expect(first.nextCursor).toBeDefined();
|
|
279
|
+
const firstBytes = first.lessons.reduce(
|
|
280
|
+
(sum, entry) => sum + Buffer.byteLength(entry.excerpt),
|
|
281
|
+
0,
|
|
282
|
+
);
|
|
283
|
+
expect(firstBytes).toBeLessThanOrEqual(LESSON_PAGE_MAX_BYTES);
|
|
284
|
+
expect(
|
|
285
|
+
meter.excerptBytes,
|
|
286
|
+
"the database returned at most one page of excerpts",
|
|
287
|
+
).toBeLessThanOrEqual(LESSON_PAGE_MAX_BYTES);
|
|
288
|
+
for (const entry of first.lessons) {
|
|
289
|
+
expect(Buffer.byteLength(entry.excerpt)).toBeLessThanOrEqual(LESSON_EXCERPT_MAX_BYTES);
|
|
290
|
+
expect(entry.excerpt).not.toContain("�");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const walkMeter = newMeter();
|
|
294
|
+
const walker = leg.make(walkMeter);
|
|
295
|
+
const pool = await readRankingPool(walker, NOW);
|
|
296
|
+
expect(pool.truncated).toBe(false);
|
|
297
|
+
expect(pool.lessons.map((entry) => entry.id).toSorted()).toEqual(activeIds.toSorted());
|
|
298
|
+
expect(walkMeter.rows, "every active row read exactly once, inactive never").toBe(
|
|
299
|
+
activeCount,
|
|
300
|
+
);
|
|
301
|
+
expect(walkMeter.payloadBytes).toBe(0);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("the ranking scan stops at its 2,000-row ceiling", async () => {
|
|
305
|
+
if (leg.label.includes("live")) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const writer = scopedWriter(newMeter());
|
|
309
|
+
for (let index = 0; index < RANKING_SCAN_MAX_ROWS + 50; index += 1) {
|
|
310
|
+
await writer(scopeOf(index)).upsertLesson(lesson(index, `tiny ${index}`));
|
|
311
|
+
}
|
|
312
|
+
const meter = newMeter();
|
|
313
|
+
const pool = await readRankingPool(leg.make(meter), NOW);
|
|
314
|
+
expect(pool.truncated).toBe(true);
|
|
315
|
+
expect(pool.lessons).toHaveLength(RANKING_SCAN_MAX_ROWS);
|
|
316
|
+
expect(meter.rows).toBe(RANKING_SCAN_MAX_ROWS);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("overview counts are SQL aggregates: a handful of rows, no payload bytes", async () => {
|
|
320
|
+
const seedMeter = newMeter();
|
|
321
|
+
const writer = scopedWriter(seedMeter);
|
|
322
|
+
const lessons = Math.round(300 * leg.scale);
|
|
323
|
+
const items: Parameters<typeof seedScoped>[1] = [];
|
|
324
|
+
for (let index = 0; index < lessons; index += 1) {
|
|
325
|
+
items.push({
|
|
326
|
+
scope: scopeOf(index),
|
|
327
|
+
write: (store) =>
|
|
328
|
+
store.upsertLesson(
|
|
329
|
+
lesson(index, "t", index % 10 === 0 ? { invalidAt: PAST } : undefined),
|
|
330
|
+
),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const statuses: SkillProposal["status"][] = ["pending", "applied", "rejected", "quarantined"];
|
|
334
|
+
for (let index = 0; index < 40; index += 1) {
|
|
335
|
+
items.push({
|
|
336
|
+
scope: scopeOf(index),
|
|
337
|
+
write: (store) => store.upsertProposal(proposal(index, statuses[index % 4])),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
await seedScoped(writer, items);
|
|
341
|
+
|
|
342
|
+
const meter = newMeter();
|
|
343
|
+
const store = leg.make(meter);
|
|
344
|
+
const counts = await store.countOverview({ now: NOW });
|
|
345
|
+
expect(counts).toEqual({
|
|
346
|
+
proposals: { pending: 10, applied: 10, rejected: 10, quarantined: 10 },
|
|
347
|
+
lessons: lessons - lessons / 10,
|
|
348
|
+
scopes: 6,
|
|
349
|
+
truncated: false,
|
|
350
|
+
});
|
|
351
|
+
expect(meter.payloadBytes).toBe(0);
|
|
352
|
+
expect(meter.rows).toBeLessThanOrEqual(8);
|
|
353
|
+
|
|
354
|
+
// The real entry point: learning.overview over the same store.
|
|
355
|
+
const calls: Array<{ ok: boolean; payload?: unknown }> = [];
|
|
356
|
+
const handlerMeter = newMeter();
|
|
357
|
+
await createLearningOverview({
|
|
358
|
+
readResolution: async () => ({}),
|
|
359
|
+
lessonStore: () => leg.make(handlerMeter),
|
|
360
|
+
now: () => NOW,
|
|
361
|
+
})({
|
|
362
|
+
params: {},
|
|
363
|
+
respond: (ok: boolean, payload?: unknown) => calls.push({ ok, payload }),
|
|
364
|
+
} as never);
|
|
365
|
+
expect(calls[0]).toMatchObject({
|
|
366
|
+
ok: true,
|
|
367
|
+
payload: { lessons: counts.lessons, scopes: 6, truncated: false },
|
|
368
|
+
});
|
|
369
|
+
expect(handlerMeter.payloadBytes).toBe(0);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("getLesson and getLessonsByIds read only the named rows", async () => {
|
|
373
|
+
const writer = scopedWriter(newMeter());
|
|
374
|
+
for (let index = 0; index < 200; index += 1) {
|
|
375
|
+
await writer(scopeOf(index)).upsertLesson(lesson(index, LONG_TEXT));
|
|
376
|
+
}
|
|
377
|
+
const meter = newMeter();
|
|
378
|
+
const store = leg.make(meter);
|
|
379
|
+
const found = await store.getLessonsByIds(["lesson-000003", "lesson-000150", "missing"]);
|
|
380
|
+
expect(found.map((entry) => entry.id).toSorted()).toEqual(["lesson-000003", "lesson-000150"]);
|
|
381
|
+
expect(meter.rows).toBe(2);
|
|
382
|
+
const one = newMeter();
|
|
383
|
+
expect((await leg.make(one).getLesson("lesson-000007"))?.id).toBe("lesson-000007");
|
|
384
|
+
expect(one.rows).toBe(1);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
PG_TABLE,
|
|
4
|
+
registerLessonStoreConformance,
|
|
5
|
+
RUN_ID,
|
|
6
|
+
TEST_SCHEMA,
|
|
7
|
+
type Backend,
|
|
8
|
+
} from "./lesson-store-conformance.test-helpers.js";
|
|
9
|
+
import {
|
|
10
|
+
createDefaultLearningPgPool,
|
|
11
|
+
PostgresLessonStore,
|
|
12
|
+
type PgPoolLike,
|
|
13
|
+
} from "./lesson-store-postgres.js";
|
|
14
|
+
|
|
15
|
+
// Live-server leg of the LessonStore conformance suite (`pnpm test:pg`): the contract the fs-jsonl,
|
|
16
|
+
// sqlite and in-memory-double legs run in lesson-store-conformance.test.ts, against a real
|
|
17
|
+
// Postgres. This leg, not the double, is the authority on real dialect behaviour.
|
|
18
|
+
const databaseUrl = process.env.OPENCLAW_DATABASE_URL;
|
|
19
|
+
const live = process.env.OPENCLAW_LIVE_PG_TEST === "1" && Boolean(databaseUrl);
|
|
20
|
+
|
|
21
|
+
type ClosablePool = PgPoolLike & { end?: () => Promise<void> };
|
|
22
|
+
let sharedPool: ClosablePool;
|
|
23
|
+
|
|
24
|
+
async function cleanupPostgresRows(): Promise<void> {
|
|
25
|
+
// The table only exists once the postgres leg has run its DDL; probing avoids swallowing a
|
|
26
|
+
// real error in a bare catch.
|
|
27
|
+
const probe = await sharedPool.query("SELECT to_regclass($1) AS reg", [PG_TABLE]);
|
|
28
|
+
if (!probe.rows[0]?.reg) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await sharedPool.query(`DELETE FROM ${PG_TABLE} WHERE workspace_id LIKE $1`, [`${RUN_ID}%`]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const liveBackend: Backend = {
|
|
35
|
+
name: "postgres",
|
|
36
|
+
label: "postgres (live server)",
|
|
37
|
+
make: async (scope) => new PostgresLessonStore({ pool: sharedPool, schema: TEST_SCHEMA, scope }),
|
|
38
|
+
snapshot: async () => {
|
|
39
|
+
const result = await sharedPool.query(
|
|
40
|
+
`SELECT * FROM ${PG_TABLE} WHERE workspace_id LIKE $1
|
|
41
|
+
ORDER BY workspace_id, agent_id, record_type, record_id`,
|
|
42
|
+
[`${RUN_ID}%`],
|
|
43
|
+
);
|
|
44
|
+
return JSON.stringify(result.rows);
|
|
45
|
+
},
|
|
46
|
+
assertSearchIndex: async () => {
|
|
47
|
+
const cols = await sharedPool.query(
|
|
48
|
+
`SELECT column_name FROM information_schema.columns
|
|
49
|
+
WHERE table_schema = $1 AND table_name = 'learning_records' AND column_name = 'search_tsv'`,
|
|
50
|
+
[TEST_SCHEMA],
|
|
51
|
+
);
|
|
52
|
+
expect(cols.rows.length).toBeGreaterThan(0);
|
|
53
|
+
const idx = await sharedPool.query(
|
|
54
|
+
`SELECT indexname FROM pg_indexes
|
|
55
|
+
WHERE schemaname = $1 AND tablename = 'learning_records' AND indexdef ILIKE '%gin%'`,
|
|
56
|
+
[TEST_SCHEMA],
|
|
57
|
+
);
|
|
58
|
+
expect(idx.rows.length).toBeGreaterThan(0);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe.skipIf(!live)("live postgres", () => {
|
|
63
|
+
beforeAll(() => {
|
|
64
|
+
sharedPool = createDefaultLearningPgPool(databaseUrl!);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(async () => {
|
|
68
|
+
await cleanupPostgresRows();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterAll(async () => {
|
|
72
|
+
await sharedPool.query(`DROP SCHEMA IF EXISTS ${TEST_SCHEMA} CASCADE`);
|
|
73
|
+
await sharedPool.end?.();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
registerLessonStoreConformance(liveBackend);
|
|
77
|
+
});
|