feat(companion): extend platform bootstrap with system snapshot option

This commit is contained in:
William Valentin
2026-02-16 19:10:20 -08:00
parent 4a3c9e7fac
commit 1b69970065
5 changed files with 111 additions and 18 deletions
+1
View File
@@ -49,6 +49,7 @@ export type {
SharedStatusInput,
HeartbeatStatusInput,
PlatformBootstrapResult,
PlatformBootstrapOptions,
PlatformPutCanvasArtifactInput,
PlatformGetCanvasArtifactInput,
PlatformDeleteCanvasArtifactInput,
+45 -5
View File
@@ -12,6 +12,7 @@ function createRuntimeMock(): {
disconnect: ReturnType<typeof vi.fn>;
dispose: ReturnType<typeof vi.fn>;
registerNode: ReturnType<typeof vi.fn>;
bootstrapNode: ReturnType<typeof vi.fn>;
getNodeCapabilities: ReturnType<typeof vi.fn>;
setNodeStatus: ReturnType<typeof vi.fn>;
setNodeLocation: ReturnType<typeof vi.fn>;
@@ -29,6 +30,14 @@ function createRuntimeMock(): {
const disconnect = vi.fn(() => undefined);
const dispose = vi.fn(() => undefined);
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 setNodeStatus = vi.fn(async () => ({ updated: true }));
const setNodeLocation = vi.fn(async () => ({ updated: true }));
@@ -47,6 +56,7 @@ function createRuntimeMock(): {
disconnect,
dispose,
registerNode,
bootstrapNode,
getNodeCapabilities,
setNodeStatus,
setNodeLocation,
@@ -67,6 +77,7 @@ function createRuntimeMock(): {
disconnect,
dispose,
registerNode,
bootstrapNode,
getNodeCapabilities,
setNodeStatus,
setNodeLocation,
@@ -170,14 +181,11 @@ describe('platform companion clients', () => {
const result = await client.bootstrap();
expect(mock.registerNode).toHaveBeenCalledOnce();
expect(mock.getNodeCapabilities).toHaveBeenCalledOnce();
expect(mock.registerNode.mock.invocationCallOrder[0]).toBeLessThan(
mock.getNodeCapabilities.mock.invocationCallOrder[0],
);
expect(mock.bootstrapNode).toHaveBeenCalledOnce();
expect(result).toEqual({
register: { registered: true },
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);
});
});
+50 -12
View File
@@ -56,6 +56,11 @@ export interface HeartbeatStatusInput {
export interface PlatformBootstrapResult {
register: NodeRegisterResult;
capabilities: NodeCapabilitiesResult;
systemCapabilities?: SystemCapabilitiesResult;
}
export interface PlatformBootstrapOptions {
includeSystemCapabilities?: boolean;
}
export interface PlatformPutCanvasArtifactInput extends Omit<PutCanvasArtifactInput, 'sessionId'> {
@@ -108,10 +113,21 @@ export class MacOSCompanionClient {
});
}
async bootstrap(): Promise<PlatformBootstrapResult> {
const register = await this.register();
const capabilities = await this.getCapabilities();
return { register, capabilities };
async bootstrap(options?: PlatformBootstrapOptions): Promise<PlatformBootstrapResult> {
const result = await this.runtime.bootstrapNode(
{
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> {
@@ -248,10 +264,21 @@ export class IOSCompanionClient {
});
}
async bootstrap(): Promise<PlatformBootstrapResult> {
const register = await this.register();
const capabilities = await this.getCapabilities();
return { register, capabilities };
async bootstrap(options?: PlatformBootstrapOptions): Promise<PlatformBootstrapResult> {
const result = await this.runtime.bootstrapNode(
{
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> {
@@ -388,10 +415,21 @@ export class AndroidCompanionClient {
});
}
async bootstrap(): Promise<PlatformBootstrapResult> {
const register = await this.register();
const capabilities = await this.getCapabilities();
return { register, capabilities };
async bootstrap(options?: PlatformBootstrapOptions): Promise<PlatformBootstrapResult> {
const result = await this.runtime.bootstrapNode(
{
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> {