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.
This commit is contained in:
Nicolò Boschi 2026-03-06 09:01:56 +01:00 committed by GitHub
parent 0ad8c2d09c
commit e4dd654ec5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 40 additions and 26 deletions

View file

@ -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()

View file

@ -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<typeof vi.spyOn<HindsightClient, 'recall'>>;
retainSpy = vi.spyOn(c, 'retain') as ReturnType<typeof vi.spyOn<HindsightClient, 'retain'>>;
// Spy on the prototype so all per-bank instances created by getClientForContext are intercepted.
recallSpy = vi.spyOn(HindsightClient.prototype, 'recall') as ReturnType<typeof vi.spyOn<HindsightClient, 'recall'>>;
retainSpy = vi.spyOn(HindsightClient.prototype, 'retain') as ReturnType<typeof vi.spyOn<HindsightClient, 'retain'>>;
}, 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('<hindsight_memories>');
});
});