feat: add Makelore Robot hardware module

This commit is contained in:
2026-08-13 23:17:22 +08:00
parent 22add3f01f
commit aba5cae286
25 changed files with 3169 additions and 72 deletions

View File

@@ -0,0 +1,323 @@
import { EventEmitter } from 'node:events';
import type { IncomingMessage, ServerResponse } from 'node:http';
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
import { join } from 'node:path';
import { createInterface } from 'node:readline';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { createAiHardwareRouteHandler } from '@electron/api/routes/ai-hardware';
type ContractServer = { process: ChildProcessWithoutNullStreams; baseUrl: string };
const OPERATION = {
createDesk: '00000000-0000-4000-8000-000000000001',
bindDevice: '00000000-0000-4000-8000-000000000002',
updateAgent: '00000000-0000-4000-8000-000000000003',
updateAssignment: '00000000-0000-4000-8000-000000000004',
conflict: '00000000-0000-4000-8000-000000000005',
recoverCredential: '00000000-0000-4000-8000-000000000006',
recoverCredentialAgain: '00000000-0000-4000-8000-000000000007',
inProgress: '00000000-0000-4000-8000-000000000009',
} as const;
function localRequest(method: string, body?: unknown): IncomingMessage {
const req = new EventEmitter();
const raw = body === undefined ? undefined : JSON.stringify(body);
Object.assign(req, {
method,
headers: raw === undefined ? {} : { 'content-length': String(Buffer.byteLength(raw)) },
[Symbol.asyncIterator]: async function* () {
if (raw !== undefined) yield Buffer.from(raw);
},
});
return req as IncomingMessage;
}
function localResponse() {
const chunks: string[] = [];
const res = new EventEmitter();
Object.assign(res, {
statusCode: 0,
setHeader: vi.fn(),
end: vi.fn((chunk?: string) => { if (chunk) chunks.push(chunk); }),
});
return {
res: res as unknown as ServerResponse,
json: () => JSON.parse(chunks.join('')) as Record<string, unknown>,
};
}
async function invoke(
handler: ReturnType<typeof createAiHardwareRouteHandler>,
method: string,
path: string,
body?: unknown,
) {
const target = localResponse();
const handled = await handler(
localRequest(method, body),
target.res,
new URL(`http://127.0.0.1${path}`),
{} as never,
);
expect(handled).toBe(true);
return target.json();
}
async function startServer(disabled = false): Promise<ContractServer> {
const serverDir = process.env.WORKS_SQUARE_SERVER_DIR;
if (!serverDir) throw new Error('WORKS_SQUARE_SERVER_DIR is required');
const script = join(serverDir, 'tests', 'contract', 'ai_hardware_contract_server.py');
const executable = process.env.WORKS_SQUARE_PYTHON ?? 'uv';
const args = process.env.WORKS_SQUARE_PYTHON
? [script]
: ['run', '--frozen', 'python', script];
if (disabled) args.push('--disabled');
const child = spawn(executable, args, {
cwd: serverDir,
env: {
...process.env,
PYTHONPATH: [serverDir, process.env.PYTHONPATH].filter(Boolean).join(process.platform === 'win32' ? ';' : ':'),
PYTHONUNBUFFERED: '1',
},
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
});
let stderr = '';
child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk: string) => { stderr += chunk; });
const lines = createInterface({ input: child.stdout });
return await new Promise<ContractServer>((resolve, reject) => {
const timer = setTimeout(() => {
child.kill();
reject(new Error(`contract server readiness timed out\n${stderr}`));
}, 15_000);
child.once('error', (error) => {
clearTimeout(timer);
reject(error);
});
child.once('exit', (code) => {
clearTimeout(timer);
reject(new Error(`contract server exited before ready (${code})\n${stderr}`));
});
lines.once('line', (line) => {
clearTimeout(timer);
try {
const ready = JSON.parse(line) as { base_url?: unknown };
if (typeof ready.base_url !== 'string') throw new Error('missing base_url');
resolve({ process: child, baseUrl: ready.base_url });
} catch (error) {
child.kill();
reject(new Error(`invalid contract server handshake: ${line}`, { cause: error }));
}
});
});
}
async function stopServer(server: ContractServer): Promise<void> {
if (server.process.exitCode !== null) return;
const exited = new Promise<void>((resolve) => server.process.once('exit', () => resolve()));
server.process.stdin.end();
const forced = setTimeout(() => server.process.kill(), 10_000);
await exited;
clearTimeout(forced);
}
describe('Makelore Main to Works Square AI hardware wire contract', () => {
let server: ContractServer | undefined;
let handler: ReturnType<typeof createAiHardwareRouteHandler>;
beforeAll(async () => {
server = await startServer();
handler = createAiHardwareRouteHandler({
apiBaseUrl: server.baseUrl,
fetchImpl: globalThis.fetch,
getAccessToken: async () => 'contract-token',
});
});
afterAll(async () => {
if (server) await stopServer(server);
});
it('uses the real authenticated overview and create-agent HTTP routes', async () => {
expect(await invoke(handler, 'GET', '/api/works/ai-hardware')).toEqual({
success: true,
data: { status: 'unprovisioned', agents: [], devices: [] },
});
const createBody = { agent_name: 'Desk', client_operation_id: OPERATION.createDesk };
const created = await invoke(handler, 'POST', '/api/works/ai-hardware/agents', createBody);
expect(created).toEqual({
success: true,
data: { id: 'agent-1', name: 'Desk', config_revision: 0 },
});
expect(await invoke(handler, 'POST', '/api/works/ai-hardware/agents', createBody)).toEqual(created);
expect(await invoke(handler, 'GET', '/api/works/ai-hardware')).toEqual({
success: true,
data: {
status: 'active',
agents: [{ id: 'agent-1', name: 'Desk', config_revision: 0 }],
devices: [],
},
});
});
it('binds a device and preserves GET/PATCH configuration ETags as numeric revisions', async () => {
const bindBody = {
activation_code: '123456', agent_id: 'agent-1', client_operation_id: OPERATION.bindDevice,
};
const bound = await invoke(handler, 'POST', '/api/works/ai-hardware/device-bindings', bindBody);
expect(bound).toEqual({
success: true,
data: { id: 'device-1', agent_id: 'agent-1', assignment_revision: 0 },
});
expect(await invoke(handler, 'POST', '/api/works/ai-hardware/device-bindings', bindBody)).toEqual(bound);
const read = await invoke(handler, 'GET', '/api/works/ai-hardware/agents/agent-1');
expect(read).toMatchObject({ success: true, revision: 0, data: { id: 'agent-1', config_revision: 0 } });
const updateBody = {
revision: 0, agent_name: 'Workshop', system_prompt: 'Build carefully',
client_operation_id: OPERATION.updateAgent,
};
const updated = await invoke(handler, 'PATCH', '/api/works/ai-hardware/agents/agent-1', updateBody);
expect(updated).toMatchObject({
success: true,
revision: 1,
data: { id: 'agent-1', name: 'Workshop', config_revision: 1, system_prompt: 'Build carefully' },
});
expect(await invoke(handler, 'PATCH', '/api/works/ai-hardware/agents/agent-1', updateBody)).toEqual(updated);
});
it('preserves assignment revisions and safe 409 Retry-After metadata', async () => {
expect(await invoke(handler, 'GET', '/api/works/ai-hardware/devices/device-1/agent-assignment')).toEqual({
success: true,
revision: 0,
data: { id: 'device-1', agent_id: 'agent-1', assignment_revision: 0 },
});
expect(await invoke(handler, 'PUT', '/api/works/ai-hardware/devices/device-1/agent-assignment', {
revision: 9, agent_id: 'agent-1', client_operation_id: OPERATION.conflict,
})).toEqual({
success: false,
status: 409,
code: 'ai_hardware_revision_conflict',
error: 'AI hardware data changed; refresh and retry',
retryable: true,
retry_after_seconds: 2,
operation_id: OPERATION.conflict,
});
const assignmentBody = {
revision: 0, agent_id: 'agent-1', client_operation_id: OPERATION.updateAssignment,
};
const assigned = await invoke(
handler, 'PUT', '/api/works/ai-hardware/devices/device-1/agent-assignment', assignmentBody,
);
expect(assigned).toEqual({
success: true,
revision: 1,
data: { id: 'device-1', agent_id: 'agent-1', assignment_revision: 1 },
});
expect(await invoke(
handler, 'PUT', '/api/works/ai-hardware/devices/device-1/agent-assignment', assignmentBody,
)).toEqual(assigned);
});
it('rejects reuse of one operation id with a different request body', async () => {
expect(await invoke(handler, 'POST', '/api/works/ai-hardware/agents', {
agent_name: 'Conflict original', client_operation_id: OPERATION.conflict,
})).toMatchObject({ success: true, data: { id: 'agent-2', name: 'Conflict original', config_revision: 0 } });
expect(await invoke(handler, 'POST', '/api/works/ai-hardware/agents', {
agent_name: 'Conflict changed', client_operation_id: OPERATION.conflict,
})).toEqual({
success: false,
status: 409,
code: 'ai_hardware_idempotency_conflict',
error: 'AI hardware request conflicts with an earlier operation',
retryable: false,
operation_id: OPERATION.conflict,
});
const overview = await invoke(handler, 'GET', '/api/works/ai-hardware');
expect(overview).toMatchObject({
success: true,
data: { agents: [
{ id: 'agent-1', name: 'Workshop', config_revision: 1 },
{ id: 'agent-2', name: 'Conflict original', config_revision: 0 },
] },
});
});
it('retries operation-in-progress once with the identical key and body', async () => {
expect(await invoke(handler, 'POST', '/api/works/ai-hardware/agents', {
agent_name: 'Retried', client_operation_id: OPERATION.inProgress,
})).toEqual({
success: true,
data: { id: 'agent-3', name: 'Retried', config_revision: 0 },
});
const overview = await invoke(handler, 'GET', '/api/works/ai-hardware');
expect(overview).toMatchObject({
success: true,
data: { agents: [
{ id: 'agent-1' },
{ id: 'agent-2' },
{ id: 'agent-3', name: 'Retried', config_revision: 0 },
] },
});
});
it('recovers credentials without an upstream body and replays safely', async () => {
const recoveryBody = { client_operation_id: OPERATION.recoverCredential };
const recovered = await invoke(
handler, 'POST', '/api/works/ai-hardware/credential-recovery', recoveryBody,
);
expect(recovered).toEqual({
success: true,
data: {
status: 'active',
agents: [
{ id: 'agent-1', name: 'Workshop', config_revision: 1 },
{ id: 'agent-2', name: 'Conflict original', config_revision: 0 },
{ id: 'agent-3', name: 'Retried', config_revision: 0 },
],
devices: [{ id: 'device-1', agent_id: 'agent-1', assignment_revision: 1 }],
},
});
expect(await invoke(
handler, 'POST', '/api/works/ai-hardware/credential-recovery', recoveryBody,
)).toEqual(recovered);
expect(await invoke(handler, 'POST', '/api/works/ai-hardware/credential-recovery', {
client_operation_id: OPERATION.recoverCredentialAgain,
})).toEqual(recovered);
});
it('maps a real FastAPI Bearer rejection to a safe local auth envelope', async () => {
const unauthorized = createAiHardwareRouteHandler({
apiBaseUrl: server!.baseUrl,
fetchImpl: globalThis.fetch,
getAccessToken: async () => 'wrong-token',
});
expect(await invoke(unauthorized, 'GET', '/api/works/ai-hardware')).toEqual({
success: false,
status: 401,
code: 'AI_HARDWARE_AUTH_REQUIRED',
error: 'Works Square sign-in is required',
retryable: false,
});
});
it('maps a real disabled FastAPI route to the product disabled state', async () => {
const disabled = await startServer(true);
try {
const disabledHandler = createAiHardwareRouteHandler({
apiBaseUrl: disabled.baseUrl,
fetchImpl: globalThis.fetch,
getAccessToken: async () => 'contract-token',
});
expect(await invoke(disabledHandler, 'GET', '/api/works/ai-hardware')).toEqual({
success: false,
status: 404,
code: 'AI_HARDWARE_DISABLED',
error: 'AI hardware module is not enabled',
retryable: false,
});
} finally {
await stopServer(disabled);
}
});
});