feat(companion): add publishHeartbeat helper across platform clients

This commit is contained in:
William Valentin
2026-02-16 18:35:58 -08:00
parent 4d29c381f7
commit 5db7beeb53
6 changed files with 71 additions and 1 deletions
+1
View File
@@ -1196,6 +1196,7 @@ Companion runtime helper:
- `IOSCompanionClient` (`platform: "ios"`, APNs push registration) - `IOSCompanionClient` (`platform: "ios"`, APNs push registration)
- `AndroidCompanionClient` (`platform: "android"`, FCM push registration) - `AndroidCompanionClient` (`platform: "android"`, FCM push registration)
- shared `bootstrap()` helper (`register` + `getCapabilities`) for startup handshakes - shared `bootstrap()` helper (`register` + `getCapabilities`) for startup handshakes
- shared `publishHeartbeat()` helper for periodic `node.status.set` updates with safe defaults
## Canvas / A2UI Foundation ## Canvas / A2UI Foundation
+15
View File
@@ -217,6 +217,21 @@
], ],
"test_status": "pnpm test:run src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing" "test_status": "pnpm test:run src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing"
}, },
"companion-platform-heartbeat-helper": {
"status": "completed",
"date": "2026-02-17",
"updated": "2026-02-17",
"summary": "Added `publishHeartbeat()` convenience methods across platform companion clients to standardize periodic status reporting with safe defaults (`statusText: heartbeat`, `powerSource: unknown`), plus test and docs updates.",
"files_modified": [
"src/companion/platformClients.ts",
"src/companion/platformClients.test.ts",
"src/companion/platformClients.integration.test.ts",
"src/companion/index.ts",
"README.md",
"docs/plans/state.json"
],
"test_status": "pnpm test:run src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing"
},
"browser-tools-activation-clarity": { "browser-tools-activation-clarity": {
"status": "completed", "status": "completed",
"date": "2026-02-17", "date": "2026-02-17",
+1
View File
@@ -41,5 +41,6 @@ export type {
PlatformClientOptions, PlatformClientOptions,
RegisterPushTokenInput, RegisterPushTokenInput,
SharedStatusInput, SharedStatusInput,
HeartbeatStatusInput,
PlatformBootstrapResult, PlatformBootstrapResult,
} from './platformClients.js'; } from './platformClients.js';
@@ -121,7 +121,7 @@ describe('platform clients integration', () => {
const boot = await client.bootstrap(); const boot = await client.bootstrap();
expect(boot.register.registered).toBe(true); expect(boot.register.registered).toBe(true);
expect(boot.capabilities.node.id).toBe('macos-e2e'); expect(boot.capabilities.node.id).toBe('macos-e2e');
const status = await client.setStatus({ const status = await client.publishHeartbeat({
appVersion: '1.0.0', appVersion: '1.0.0',
statusText: 'menu-bar-active', statusText: 'menu-bar-active',
powerSource: 'ac', powerSource: 'ac',
+15
View File
@@ -168,4 +168,19 @@ describe('platform companion clients', () => {
capabilities: expect.any(Object), capabilities: expect.any(Object),
}); });
}); });
it('publishHeartbeat uses safe defaults for status payload', async () => {
const mock = createRuntimeMock();
const client = new AndroidCompanionClient({ runtime: mock.runtime, nodeId: 'android-node' });
await client.publishHeartbeat();
expect(mock.setNodeStatus).toHaveBeenCalledWith(
expect.objectContaining({
platform: 'android',
statusText: 'heartbeat',
powerSource: 'unknown',
}),
);
});
}); });
+38
View File
@@ -38,6 +38,14 @@ export type SharedStatusInput = Omit<
'platform' 'platform'
>; >;
export interface HeartbeatStatusInput {
appVersion?: SharedStatusInput['appVersion'];
deviceName?: SharedStatusInput['deviceName'];
statusText?: SharedStatusInput['statusText'];
batteryPct?: SharedStatusInput['batteryPct'];
powerSource?: SharedStatusInput['powerSource'];
}
export interface PlatformBootstrapResult { export interface PlatformBootstrapResult {
register: NodeRegisterResult; register: NodeRegisterResult;
capabilities: NodeCapabilitiesResult; capabilities: NodeCapabilitiesResult;
@@ -96,6 +104,16 @@ export class MacOSCompanionClient {
}); });
} }
publishHeartbeat(input: HeartbeatStatusInput = {}): Promise<NodeStatusSetResult> {
return this.setStatus({
appVersion: input.appVersion,
deviceName: input.deviceName,
statusText: input.statusText ?? 'heartbeat',
batteryPct: input.batteryPct,
powerSource: input.powerSource ?? 'unknown',
});
}
setLocation(location: SetNodeLocationInput): Promise<NodeLocationSetResult> { setLocation(location: SetNodeLocationInput): Promise<NodeLocationSetResult> {
return this.runtime.setNodeLocation(location); return this.runtime.setNodeLocation(location);
} }
@@ -195,6 +213,16 @@ export class IOSCompanionClient {
}); });
} }
publishHeartbeat(input: HeartbeatStatusInput = {}): Promise<NodeStatusSetResult> {
return this.setStatus({
appVersion: input.appVersion,
deviceName: input.deviceName,
statusText: input.statusText ?? 'heartbeat',
batteryPct: input.batteryPct,
powerSource: input.powerSource ?? 'unknown',
});
}
setLocation(location: SetNodeLocationInput): Promise<NodeLocationSetResult> { setLocation(location: SetNodeLocationInput): Promise<NodeLocationSetResult> {
return this.runtime.setNodeLocation(location); return this.runtime.setNodeLocation(location);
} }
@@ -294,6 +322,16 @@ export class AndroidCompanionClient {
}); });
} }
publishHeartbeat(input: HeartbeatStatusInput = {}): Promise<NodeStatusSetResult> {
return this.setStatus({
appVersion: input.appVersion,
deviceName: input.deviceName,
statusText: input.statusText ?? 'heartbeat',
batteryPct: input.batteryPct,
powerSource: input.powerSource ?? 'unknown',
});
}
setLocation(location: SetNodeLocationInput): Promise<NodeLocationSetResult> { setLocation(location: SetNodeLocationInput): Promise<NodeLocationSetResult> {
return this.runtime.setNodeLocation(location); return this.runtime.setNodeLocation(location);
} }