Add integration coverage for companion platform clients

This commit is contained in:
William Valentin
2026-02-16 13:56:45 -08:00
parent ce9af106ff
commit 52231b7a93
3 changed files with 225 additions and 2 deletions
@@ -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`
+15 -2
View File
@@ -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",
@@ -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<boolean> {
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();
}
});
});