From e4dd654ec5c83cab2bed35de45cff0240e3c73ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Fri, 6 Mar 2026 09:01:56 +0100 Subject: [PATCH] fix: openclaw tests + split doc-examples CI per language (#503) * fix: update openclaw tests to use before_prompt_build hook and split doc-examples CI per language - Update hooks.integration.test.ts: rename describe block and all triggerHook calls from 'before_agent_start' to 'before_prompt_build' to match the hook registered in index.ts (changed in PR #480) - Fix 'includes the user message' test: prependContext contains memories (bullet list), not the raw user query; update assertion accordingly - Split test-doc-examples CI job into a matrix over [python, node, cli, go] so each language runs in parallel; language-specific setup steps (Rust/CLI build, Node.js, Python client, TypeScript client) are conditional on matrix.language to avoid unnecessary work * fix: spy on HindsightClient prototype to intercept all per-bank client instances getClientForContext creates new HindsightClient instances per bank when dynamicBankId is true, so vi.spyOn(c, 'recall') on the default client never captured calls. Spy on HindsightClient.prototype instead so all dynamically created bank clients are intercepted. --- .github/workflows/test.yml | 16 +++++- .../openclaw/tests/hooks.integration.test.ts | 50 ++++++++++--------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14ba5588..e0f5c835 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1321,6 +1321,11 @@ jobs: test-doc-examples: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + language: [python, node, cli, go] + name: test-doc-examples (${{ matrix.language }}) env: HINDSIGHT_API_LLM_PROVIDER: vertexai HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY: /tmp/gcp-credentials.json @@ -1339,9 +1344,11 @@ jobs: echo "HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=$PROJECT_ID" >> $GITHUB_ENV - name: Install Rust + if: matrix.language == 'cli' uses: dtolnay/rust-toolchain@stable - name: Cache cargo + if: matrix.language == 'cli' uses: actions/cache@v4 with: path: | @@ -1351,6 +1358,7 @@ jobs: key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Build CLI + if: matrix.language == 'cli' working-directory: hindsight-cli run: | cargo build --release @@ -1368,6 +1376,7 @@ jobs: python-version-file: ".python-version" - name: Set up Node.js + if: matrix.language == 'node' uses: actions/setup-node@v4 with: node-version: '20' @@ -1381,10 +1390,12 @@ jobs: uv sync --frozen --no-install-project --index-strategy unsafe-best-match - name: Install Python client dependencies + if: matrix.language == 'python' working-directory: ./hindsight-clients/python run: uv sync --frozen --extra test --index-strategy unsafe-best-match - name: Install TypeScript client + if: matrix.language == 'node' run: | npm ci --workspace=hindsight-clients/typescript npm run build --workspace=hindsight-clients/typescript @@ -1436,10 +1447,11 @@ jobs: done - name: Configure CLI + if: matrix.language == 'cli' run: hindsight configure --api-url http://localhost:8888 - - name: Run all doc examples - run: ./scripts/test-doc-examples.sh + - name: Run doc examples (${{ matrix.language }}) + run: ./scripts/test-doc-examples.sh --lang ${{ matrix.language }} - name: Show API server logs if: always() diff --git a/hindsight-integrations/openclaw/tests/hooks.integration.test.ts b/hindsight-integrations/openclaw/tests/hooks.integration.test.ts index 3f89ca70..734353fa 100644 --- a/hindsight-integrations/openclaw/tests/hooks.integration.test.ts +++ b/hindsight-integrations/openclaw/tests/hooks.integration.test.ts @@ -2,7 +2,7 @@ * Integration tests for the OpenClaw plugin hooks. * * Loads the plugin with a mock MoltbotPluginAPI in HTTP mode, then triggers - * `before_agent_start` and `agent_end` hooks with realistic event payloads. + * `before_prompt_build` and `agent_end` hooks with realistic event payloads. * Client methods (recall / retain) are spied on to verify the plugin * orchestrates them correctly without requiring a full LLM pipeline. * @@ -134,6 +134,7 @@ beforeAll(async () => { process.env.HINDSIGHT_EMBED_API_URL = HINDSIGHT_API_URL; const mod = await import('../src/index.js'); + const { HindsightClient } = await import('../src/client.js'); const pluginFn = mod.default; const getClient = mod.getClient; @@ -156,11 +157,11 @@ beforeAll(async () => { await handle.startServices(); // After startServices the client must be ready. - const c = getClient(); - if (!c) throw new Error('[Hooks Integration] Client not initialized after service start'); + if (!getClient()) throw new Error('[Hooks Integration] Client not initialized after service start'); - recallSpy = vi.spyOn(c, 'recall') as ReturnType>; - retainSpy = vi.spyOn(c, 'retain') as ReturnType>; + // Spy on the prototype so all per-bank instances created by getClientForContext are intercepted. + recallSpy = vi.spyOn(HindsightClient.prototype, 'recall') as ReturnType>; + retainSpy = vi.spyOn(HindsightClient.prototype, 'retain') as ReturnType>; }, 30_000); afterAll(async () => { @@ -181,13 +182,13 @@ afterEach(() => { // before_agent_start // --------------------------------------------------------------------------- -describe('before_agent_start hook', () => { +describe('before_prompt_build hook', () => { it('skips recall for excluded providers and returns undefined', async () => { if (!apiReachable) return; const result = await triggerHook( - 'before_agent_start', - { rawMessage: 'What are my preferences?', prompt: 'What are my preferences?' }, + 'before_prompt_build', + { rawMessage: 'What are my preferences?', prompt: 'What are my preferences?', messages: [] }, { messageProvider: 'slack', senderId: 'U001' }, ); @@ -199,8 +200,8 @@ describe('before_agent_start hook', () => { if (!apiReachable) return; const result = await triggerHook( - 'before_agent_start', - { rawMessage: 'Hi', prompt: 'Hi' }, + 'before_prompt_build', + { rawMessage: 'Hi', prompt: 'Hi', messages: [] }, { messageProvider: 'telegram', senderId: 'U001' }, ); @@ -213,8 +214,8 @@ describe('before_agent_start hook', () => { recallSpy.mockResolvedValue(EMPTY_RECALL); const result = await triggerHook( - 'before_agent_start', - { rawMessage: 'What programming language do I like?', prompt: '' }, + 'before_prompt_build', + { rawMessage: 'What programming language do I like?', prompt: '', messages: [] }, { messageProvider: 'telegram', senderId: 'U002' }, ); @@ -232,8 +233,8 @@ describe('before_agent_start hook', () => { }); const result = (await triggerHook( - 'before_agent_start', - { rawMessage: 'What programming language do I prefer?', prompt: '' }, + 'before_prompt_build', + { rawMessage: 'What programming language do I prefer?', prompt: '', messages: [] }, { messageProvider: 'telegram', senderId: 'U003' }, )) as { prependContext: string }; @@ -256,8 +257,8 @@ describe('before_agent_start hook', () => { }); const result = (await triggerHook( - 'before_agent_start', - { rawMessage: 'Do I prefer dark or light mode?', prompt: '' }, + 'before_prompt_build', + { rawMessage: 'Do I prefer dark or light mode?', prompt: '', messages: [] }, { messageProvider: 'telegram', senderId: 'U004' }, )) as { prependContext: string }; @@ -273,8 +274,8 @@ describe('before_agent_start hook', () => { const envelopePrompt = '[Telegram Chat]\nWhat is my favorite food?\n[from: Alice]'; await triggerHook( - 'before_agent_start', - { rawMessage: '', prompt: envelopePrompt }, + 'before_prompt_build', + { rawMessage: '', prompt: envelopePrompt, messages: [] }, { messageProvider: 'telegram', senderId: 'U005' }, ); @@ -317,8 +318,8 @@ describe('before_agent_start hook', () => { recallSpy.mockResolvedValue(EMPTY_RECALL); await triggerHook( - 'before_agent_start', - { rawMessage: 'Tell me about my hobbies please.', prompt: '' }, + 'before_prompt_build', + { rawMessage: 'Tell me about my hobbies please.', prompt: '', messages: [] }, { messageProvider: 'telegram', senderId: 'U006' }, ); @@ -327,7 +328,7 @@ describe('before_agent_start hook', () => { expect(callArgs.max_tokens).toBeGreaterThan(0); }); - it('includes the user message in the prependContext block', async () => { + it('includes recalled memories in the prependContext block', async () => { if (!apiReachable) return; recallSpy.mockResolvedValue({ results: [makeMemoryResult('User loves hiking')], @@ -337,12 +338,13 @@ describe('before_agent_start hook', () => { }); const result = (await triggerHook( - 'before_agent_start', - { rawMessage: 'What outdoor activities do I enjoy?', prompt: '' }, + 'before_prompt_build', + { rawMessage: 'What outdoor activities do I enjoy?', prompt: '', messages: [] }, { messageProvider: 'telegram', senderId: 'U007' }, )) as { prependContext: string }; - expect(result.prependContext).toContain('What outdoor activities do I enjoy?'); + expect(result.prependContext).toContain('User loves hiking'); + expect(result.prependContext).toContain(''); }); });