feat(companion): extend platform bootstrap with system snapshot option
This commit is contained in:
@@ -1195,7 +1195,7 @@ Companion runtime helper:
|
|||||||
- `MacOSCompanionClient` (`platform: "macos"`, APNs push registration)
|
- `MacOSCompanionClient` (`platform: "macos"`, APNs push registration)
|
||||||
- `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`, optional `system.capabilities`) for startup handshakes
|
||||||
- shared `publishHeartbeat()` helper for periodic `node.status.set` updates with safe defaults
|
- shared `publishHeartbeat()` helper for periodic `node.status.set` updates with safe defaults
|
||||||
- `createHeartbeatLoop()` convenience helper that returns a bound `CompanionHeartbeatLoop`
|
- `createHeartbeatLoop()` convenience helper that returns a bound `CompanionHeartbeatLoop`
|
||||||
- optional `defaultSessionId` for canvas helper calls so `sessionId` can be omitted per call
|
- optional `defaultSessionId` for canvas helper calls so `sessionId` can be omitted per call
|
||||||
|
|||||||
@@ -500,6 +500,20 @@
|
|||||||
],
|
],
|
||||||
"test_status": "pnpm test:run src/companion/heartbeatLoop.test.ts src/companion/runtimeClient.test.ts src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing"
|
"test_status": "pnpm test:run src/companion/heartbeatLoop.test.ts src/companion/runtimeClient.test.ts src/companion/platformClients.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing"
|
||||||
},
|
},
|
||||||
|
"companion-platform-bootstrap-system-capabilities-option": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"updated": "2026-02-17",
|
||||||
|
"summary": "Extended platform client `bootstrap()` to delegate to `runtime.bootstrapNode()` and optionally include `system.capabilities` in bootstrap responses.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/companion/platformClients.ts",
|
||||||
|
"src/companion/platformClients.test.ts",
|
||||||
|
"src/companion/index.ts",
|
||||||
|
"README.md",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/companion/platformClients.test.ts src/companion/runtimeClient.test.ts src/companion/heartbeatLoop.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",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type {
|
|||||||
SharedStatusInput,
|
SharedStatusInput,
|
||||||
HeartbeatStatusInput,
|
HeartbeatStatusInput,
|
||||||
PlatformBootstrapResult,
|
PlatformBootstrapResult,
|
||||||
|
PlatformBootstrapOptions,
|
||||||
PlatformPutCanvasArtifactInput,
|
PlatformPutCanvasArtifactInput,
|
||||||
PlatformGetCanvasArtifactInput,
|
PlatformGetCanvasArtifactInput,
|
||||||
PlatformDeleteCanvasArtifactInput,
|
PlatformDeleteCanvasArtifactInput,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ function createRuntimeMock(): {
|
|||||||
disconnect: ReturnType<typeof vi.fn>;
|
disconnect: ReturnType<typeof vi.fn>;
|
||||||
dispose: ReturnType<typeof vi.fn>;
|
dispose: ReturnType<typeof vi.fn>;
|
||||||
registerNode: ReturnType<typeof vi.fn>;
|
registerNode: ReturnType<typeof vi.fn>;
|
||||||
|
bootstrapNode: ReturnType<typeof vi.fn>;
|
||||||
getNodeCapabilities: ReturnType<typeof vi.fn>;
|
getNodeCapabilities: ReturnType<typeof vi.fn>;
|
||||||
setNodeStatus: ReturnType<typeof vi.fn>;
|
setNodeStatus: ReturnType<typeof vi.fn>;
|
||||||
setNodeLocation: ReturnType<typeof vi.fn>;
|
setNodeLocation: ReturnType<typeof vi.fn>;
|
||||||
@@ -29,6 +30,14 @@ function createRuntimeMock(): {
|
|||||||
const disconnect = vi.fn(() => undefined);
|
const disconnect = vi.fn(() => undefined);
|
||||||
const dispose = vi.fn(() => undefined);
|
const dispose = vi.fn(() => undefined);
|
||||||
const registerNode = vi.fn(async () => ({ registered: true }));
|
const registerNode = vi.fn(async () => ({ registered: true }));
|
||||||
|
const bootstrapNode = vi.fn(async () => ({
|
||||||
|
register: { registered: true },
|
||||||
|
capabilities: {
|
||||||
|
node: { id: 'n1', role: 'companion', registeredAt: Date.now() },
|
||||||
|
protocol: { serverVersion: 1, nodeVersion: 1, negotiatedVersion: 1 },
|
||||||
|
capabilities: { declared: [], enabled: [], featureGates: {} },
|
||||||
|
},
|
||||||
|
}));
|
||||||
const getNodeCapabilities = vi.fn(async () => ({ node: { id: 'n1', role: 'companion', registeredAt: Date.now() }, protocol: { serverVersion: 1, nodeVersion: 1, negotiatedVersion: 1 }, capabilities: { declared: [], enabled: [], featureGates: {} } }));
|
const getNodeCapabilities = vi.fn(async () => ({ node: { id: 'n1', role: 'companion', registeredAt: Date.now() }, protocol: { serverVersion: 1, nodeVersion: 1, negotiatedVersion: 1 }, capabilities: { declared: [], enabled: [], featureGates: {} } }));
|
||||||
const setNodeStatus = vi.fn(async () => ({ updated: true }));
|
const setNodeStatus = vi.fn(async () => ({ updated: true }));
|
||||||
const setNodeLocation = vi.fn(async () => ({ updated: true }));
|
const setNodeLocation = vi.fn(async () => ({ updated: true }));
|
||||||
@@ -47,6 +56,7 @@ function createRuntimeMock(): {
|
|||||||
disconnect,
|
disconnect,
|
||||||
dispose,
|
dispose,
|
||||||
registerNode,
|
registerNode,
|
||||||
|
bootstrapNode,
|
||||||
getNodeCapabilities,
|
getNodeCapabilities,
|
||||||
setNodeStatus,
|
setNodeStatus,
|
||||||
setNodeLocation,
|
setNodeLocation,
|
||||||
@@ -67,6 +77,7 @@ function createRuntimeMock(): {
|
|||||||
disconnect,
|
disconnect,
|
||||||
dispose,
|
dispose,
|
||||||
registerNode,
|
registerNode,
|
||||||
|
bootstrapNode,
|
||||||
getNodeCapabilities,
|
getNodeCapabilities,
|
||||||
setNodeStatus,
|
setNodeStatus,
|
||||||
setNodeLocation,
|
setNodeLocation,
|
||||||
@@ -170,14 +181,11 @@ describe('platform companion clients', () => {
|
|||||||
|
|
||||||
const result = await client.bootstrap();
|
const result = await client.bootstrap();
|
||||||
|
|
||||||
expect(mock.registerNode).toHaveBeenCalledOnce();
|
expect(mock.bootstrapNode).toHaveBeenCalledOnce();
|
||||||
expect(mock.getNodeCapabilities).toHaveBeenCalledOnce();
|
|
||||||
expect(mock.registerNode.mock.invocationCallOrder[0]).toBeLessThan(
|
|
||||||
mock.getNodeCapabilities.mock.invocationCallOrder[0],
|
|
||||||
);
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
register: { registered: true },
|
register: { registered: true },
|
||||||
capabilities: expect.any(Object),
|
capabilities: expect.any(Object),
|
||||||
|
systemCapabilities: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,4 +258,36 @@ describe('platform companion clients', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('bootstrap can request system capabilities snapshot', async () => {
|
||||||
|
const mock = createRuntimeMock();
|
||||||
|
mock.bootstrapNode.mockResolvedValueOnce({
|
||||||
|
register: { registered: true },
|
||||||
|
capabilities: {
|
||||||
|
node: { id: 'n1', role: 'companion', registeredAt: Date.now() },
|
||||||
|
protocol: { serverVersion: 1, nodeVersion: 1, negotiatedVersion: 1 },
|
||||||
|
capabilities: { declared: [], enabled: [], featureGates: {} },
|
||||||
|
},
|
||||||
|
systemCapabilities: {
|
||||||
|
protocol: { version: 1 },
|
||||||
|
nodes: {
|
||||||
|
enabled: true,
|
||||||
|
locationEnabled: true,
|
||||||
|
pushEnabled: true,
|
||||||
|
allowedRoles: ['companion'],
|
||||||
|
registered: true,
|
||||||
|
},
|
||||||
|
featureGates: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const client = new MacOSCompanionClient({ runtime: mock.runtime, nodeId: 'mac-node' });
|
||||||
|
|
||||||
|
const result = await client.bootstrap({ includeSystemCapabilities: true });
|
||||||
|
|
||||||
|
expect(mock.bootstrapNode).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ nodeId: 'mac-node' }),
|
||||||
|
{ includeSystemCapabilities: true },
|
||||||
|
);
|
||||||
|
expect(result.systemCapabilities?.nodes.enabled).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ export interface HeartbeatStatusInput {
|
|||||||
export interface PlatformBootstrapResult {
|
export interface PlatformBootstrapResult {
|
||||||
register: NodeRegisterResult;
|
register: NodeRegisterResult;
|
||||||
capabilities: NodeCapabilitiesResult;
|
capabilities: NodeCapabilitiesResult;
|
||||||
|
systemCapabilities?: SystemCapabilitiesResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlatformBootstrapOptions {
|
||||||
|
includeSystemCapabilities?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlatformPutCanvasArtifactInput extends Omit<PutCanvasArtifactInput, 'sessionId'> {
|
export interface PlatformPutCanvasArtifactInput extends Omit<PutCanvasArtifactInput, 'sessionId'> {
|
||||||
@@ -108,10 +113,21 @@ export class MacOSCompanionClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async bootstrap(): Promise<PlatformBootstrapResult> {
|
async bootstrap(options?: PlatformBootstrapOptions): Promise<PlatformBootstrapResult> {
|
||||||
const register = await this.register();
|
const result = await this.runtime.bootstrapNode(
|
||||||
const capabilities = await this.getCapabilities();
|
{
|
||||||
return { register, capabilities };
|
nodeId: this.nodeId,
|
||||||
|
role: this.role,
|
||||||
|
protocolVersion: this.protocolVersion,
|
||||||
|
capabilities: this.capabilities,
|
||||||
|
},
|
||||||
|
{ includeSystemCapabilities: options?.includeSystemCapabilities },
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
register: result.register,
|
||||||
|
capabilities: result.capabilities,
|
||||||
|
systemCapabilities: result.systemCapabilities,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getCapabilities(): Promise<NodeCapabilitiesResult> {
|
getCapabilities(): Promise<NodeCapabilitiesResult> {
|
||||||
@@ -248,10 +264,21 @@ export class IOSCompanionClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async bootstrap(): Promise<PlatformBootstrapResult> {
|
async bootstrap(options?: PlatformBootstrapOptions): Promise<PlatformBootstrapResult> {
|
||||||
const register = await this.register();
|
const result = await this.runtime.bootstrapNode(
|
||||||
const capabilities = await this.getCapabilities();
|
{
|
||||||
return { register, capabilities };
|
nodeId: this.nodeId,
|
||||||
|
role: this.role,
|
||||||
|
protocolVersion: this.protocolVersion,
|
||||||
|
capabilities: this.capabilities,
|
||||||
|
},
|
||||||
|
{ includeSystemCapabilities: options?.includeSystemCapabilities },
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
register: result.register,
|
||||||
|
capabilities: result.capabilities,
|
||||||
|
systemCapabilities: result.systemCapabilities,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getCapabilities(): Promise<NodeCapabilitiesResult> {
|
getCapabilities(): Promise<NodeCapabilitiesResult> {
|
||||||
@@ -388,10 +415,21 @@ export class AndroidCompanionClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async bootstrap(): Promise<PlatformBootstrapResult> {
|
async bootstrap(options?: PlatformBootstrapOptions): Promise<PlatformBootstrapResult> {
|
||||||
const register = await this.register();
|
const result = await this.runtime.bootstrapNode(
|
||||||
const capabilities = await this.getCapabilities();
|
{
|
||||||
return { register, capabilities };
|
nodeId: this.nodeId,
|
||||||
|
role: this.role,
|
||||||
|
protocolVersion: this.protocolVersion,
|
||||||
|
capabilities: this.capabilities,
|
||||||
|
},
|
||||||
|
{ includeSystemCapabilities: options?.includeSystemCapabilities },
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
register: result.register,
|
||||||
|
capabilities: result.capabilities,
|
||||||
|
systemCapabilities: result.systemCapabilities,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getCapabilities(): Promise<NodeCapabilitiesResult> {
|
getCapabilities(): Promise<NodeCapabilitiesResult> {
|
||||||
|
|||||||
Reference in New Issue
Block a user