From 52231b7a93792bf09323935b9d95f119d3a5fd24 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 16 Feb 2026 13:56:45 -0800 Subject: [PATCH] Add integration coverage for companion platform clients --- ...-clients-integration-coverage-checklist.md | 20 ++ docs/plans/state.json | 17 +- .../platformClients.integration.test.ts | 190 ++++++++++++++++++ 3 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 docs/plans/2026-02-16-companion-platform-clients-integration-coverage-checklist.md create mode 100644 src/companion/platformClients.integration.test.ts diff --git a/docs/plans/2026-02-16-companion-platform-clients-integration-coverage-checklist.md b/docs/plans/2026-02-16-companion-platform-clients-integration-coverage-checklist.md new file mode 100644 index 0000000..736cf8b --- /dev/null +++ b/docs/plans/2026-02-16-companion-platform-clients-integration-coverage-checklist.md @@ -0,0 +1,20 @@ +# Companion Platform Clients Integration Coverage Checklist (2026-02-16) + +## Scope + +- Add gateway-backed integration coverage for platform wrappers so behavior is validated against live JSON-RPC handling, not only mocks. + +## Implementation + +- Added `src/companion/platformClients.integration.test.ts`. +- Test harness spins up a real `GatewayServer` fixture with node RPC enabled and token auth. +- Each platform wrapper is exercised end-to-end: + - macOS: registration + status (`platform: macos`) + `system.nodes` visibility + - iOS: registration + APNs push registration + `system.nodes` visibility + - Android: registration + FCM push registration + `system.nodes` visibility + +## Validation + +- `pnpm test:run src/companion/platformClients.integration.test.ts src/companion/platformClients.test.ts src/companion/runtimeClient.test.ts` +- `pnpm typecheck` +- `pnpm build` diff --git a/docs/plans/state.json b/docs/plans/state.json index ed8594f..41f56d7 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -744,6 +744,19 @@ ], "test_status": "pnpm test:run src/companion/platformClients.test.ts src/companion/runtimeClient.test.ts + pnpm typecheck + pnpm build passing" }, + "companion-platform-clients-integration-coverage": { + "file": "2026-02-16-companion-platform-clients-integration-coverage-checklist.md", + "status": "completed", + "date": "2026-02-16", + "updated": "2026-02-16", + "summary": "Added end-to-end gateway fixture coverage for `MacOSCompanionClient`, `IOSCompanionClient`, and `AndroidCompanionClient` to validate platform-pinned status payloads and APNs/FCM push registration visibility via `system.nodes`.", + "files_created": [ + "docs/plans/2026-02-16-companion-platform-clients-integration-coverage-checklist.md", + "src/companion/platformClients.integration.test.ts" + ], + "files_modified": [], + "test_status": "pnpm test:run src/companion/platformClients.integration.test.ts src/companion/platformClients.test.ts src/companion/runtimeClient.test.ts + pnpm typecheck + pnpm build passing" + }, "qmd-backend": { "file": "2026-02-16-qmd-backend-checklist.md", "status": "completed", @@ -3305,7 +3318,7 @@ } }, "overall_progress": { - "total_test_count": 1820, + "total_test_count": 1823, "all_tests_passing": true, "p0_completion": "3/3 (100%)", "p1_completion": "4/4 (100%)", @@ -3325,7 +3338,7 @@ "gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram", "native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback", "remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 3/3 (100%) — component registry, confidence routing, history index. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening", - "next_up": "OpenClaw gap: integrate companion platform clients into concrete app runtimes and add end-to-end gateway fixture coverage" + "next_up": "OpenClaw gap: wire companion platform clients into concrete macOS/iOS/Android runtime app entrypoints" }, "soul_md_and_cron_create": { "date": "2026-02-11", diff --git a/src/companion/platformClients.integration.test.ts b/src/companion/platformClients.integration.test.ts new file mode 100644 index 0000000..61fab2c --- /dev/null +++ b/src/companion/platformClients.integration.test.ts @@ -0,0 +1,190 @@ +import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; +import { resolve } from 'path'; +import { createServer } from 'net'; +import type { GatewayServerConfig } from '../gateway/server.js'; +import { GatewayServer } from '../gateway/server.js'; +import { CompanionRuntimeClient } from './runtimeClient.js'; +import { + AndroidCompanionClient, + IOSCompanionClient, + MacOSCompanionClient, +} from './platformClients.js'; + +async function canListenOnLocalhost(): Promise { + return new Promise((resolvePromise) => { + const s = createServer(); + s.once('error', () => resolvePromise(false)); + s.listen(0, '127.0.0.1', () => { + s.close(() => resolvePromise(true)); + }); + }); +} + +const mockSession = { + id: 'test', + addMessage: vi.fn(), + getHistory: vi.fn(() => []), + clear: vi.fn(), + setHistory: vi.fn(), + replaceHistory: vi.fn(), +}; + +const mockSessionManager = { + getSession: vi.fn(() => mockSession), + listSessions: vi.fn(() => ['ws:test']), + transferSession: vi.fn(), + closeSession: vi.fn(), +}; + +const mockModelClient = { + chat: vi.fn(async () => ({ + content: 'Hello from Flynn!', + stopReason: 'end_turn', + usage: { inputTokens: 10, outputTokens: 5 }, + })), +}; + +const mockToolRegistry = { + register: vi.fn(), + get: vi.fn(), + list: vi.fn(() => []), + filteredList: vi.fn(() => []), + toAnthropicFormat: vi.fn(() => []), + toOpenAIFormat: vi.fn(() => []), + filteredToAnthropicFormat: vi.fn(() => []), + filteredToOpenAIFormat: vi.fn(() => []), +}; + +const mockToolExecutor = { + execute: vi.fn(async () => ({ success: true, output: 'ok' })), +}; + +const TEST_PORT = 18912; +const TEST_TOKEN = 'platform-clients-token'; + +let LISTEN_ALLOWED = true; +let server: GatewayServer; + +beforeAll(async () => { + LISTEN_ALLOWED = await canListenOnLocalhost(); + if (!LISTEN_ALLOWED) { + return; + } + + server = new GatewayServer({ + port: TEST_PORT, + sessionManager: mockSessionManager as unknown as GatewayServerConfig['sessionManager'], + modelClient: mockModelClient, + systemPrompt: 'Test prompt', + toolRegistry: mockToolRegistry as unknown as GatewayServerConfig['toolRegistry'], + toolExecutor: mockToolExecutor as unknown as GatewayServerConfig['toolExecutor'], + version: '0.1.0-test', + uiDir: resolve(import.meta.dirname, '../gateway/ui'), + auth: { token: TEST_TOKEN }, + nodes: { + enabled: true, + allowedRoles: ['companion'], + featureGates: { 'ui.canvas': true }, + locationEnabled: true, + pushEnabled: true, + }, + }); + + await server.start(); +}); + +afterAll(async () => { + if (!LISTEN_ALLOWED) { + return; + } + await server.stop(); +}); + +function createRuntime(): CompanionRuntimeClient { + return new CompanionRuntimeClient({ + url: `ws://127.0.0.1:${TEST_PORT}`, + token: TEST_TOKEN, + }); +} + +describe('platform clients integration', () => { + it('macOS companion wrapper registers and writes status with platform pinning', async () => { + if (!LISTEN_ALLOWED) { + return; + } + + const runtime = createRuntime(); + const client = new MacOSCompanionClient({ runtime, nodeId: 'macos-e2e' }); + await client.connect(); + + try { + await client.register(); + const status = await client.setStatus({ + appVersion: '1.0.0', + statusText: 'menu-bar-active', + powerSource: 'ac', + }); + + expect(status.updated).toBe(true); + expect(status.status.platform).toBe('macos'); + + const nodes = await client.listNodes(); + const entry = nodes.nodes.find((n) => n.nodeId === 'macos-e2e'); + expect(entry?.status?.platform).toBe('macos'); + } finally { + client.disconnect(); + } + }); + + it('iOS companion wrapper uses APNs push flow', async () => { + if (!LISTEN_ALLOWED) { + return; + } + + const runtime = createRuntime(); + const client = new IOSCompanionClient({ runtime, nodeId: 'ios-e2e' }); + await client.connect(); + + try { + await client.register(); + const push = await client.registerPushToken({ + token: 'd'.repeat(64), + topic: 'dev.flynn.ios', + environment: 'sandbox', + }); + + expect(push.updated).toBe(true); + expect(push.push.provider).toBe('apns'); + + const nodes = await client.listNodes(); + const entry = nodes.nodes.find((n) => n.nodeId === 'ios-e2e'); + expect(entry?.push?.provider).toBe('apns'); + } finally { + client.disconnect(); + } + }); + + it('Android companion wrapper uses FCM push flow', async () => { + if (!LISTEN_ALLOWED) { + return; + } + + const runtime = createRuntime(); + const client = new AndroidCompanionClient({ runtime, nodeId: 'android-e2e' }); + await client.connect(); + + try { + await client.register(); + const push = await client.registerPushToken('e'.repeat(64)); + + expect(push.updated).toBe(true); + expect(push.push.provider).toBe('fcm'); + + const nodes = await client.listNodes(); + const entry = nodes.nodes.find((n) => n.nodeId === 'android-e2e'); + expect(entry?.push?.provider).toBe('fcm'); + } finally { + client.disconnect(); + } + }); +});