feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s

Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
+147
View File
@@ -0,0 +1,147 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import vm from 'node:vm';
const require = createRequire(import.meta.url);
const ts = require('typescript');
const source = readFileSync(process.env.API_CLIENT_TEST_SOURCE || new URL('../src/api/client.ts', import.meta.url), 'utf8');
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText;
function loadClient(fetchImpl = fetch) {
const timers = new Map();
const delays = [];
const events = [];
let sequence = 0;
let notifyHeaders;
const headers = new Promise(resolve => { notifyHeaders = resolve; });
const exports = {};
vm.runInNewContext(compiled, {
exports,
require: name => {
assert.equal(name, '../auth/portal');
return { isOperatorPortal: () => false, phoneCSRFKey: () => 'test-csrf' };
},
AbortController, DOMException, Event, URLSearchParams,
sessionStorage: { getItem: () => null },
window: {
setTimeout: (callback, delay) => { const id = ++sequence; timers.set(id, callback); delays.push(delay); return id; },
clearTimeout: id => timers.delete(id),
dispatchEvent: event => { events.push(event.type); return true; }
},
fetch: async (...args) => { const response = await fetchImpl(...args); notifyHeaders(); return response; }
});
return {
...exports, timers, delays, events, headers,
expire: () => {
assert.equal(timers.size, 1, 'deadline must remain active after headers');
timers.values().next().value();
}
};
}
async function stalledResponse(t, status = 200) {
const server = createServer((_request, response) => {
response.writeHead(status, { 'Content-Type': 'application/json' });
response.flushHeaders();
response.write('{');
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
t.after(() => {
server.closeAllConnections();
return new Promise(resolve => server.close(resolve));
});
return `http://127.0.0.1:${server.address().port}/api/v1/subjects/test`;
}
async function waitForBody(api) {
await api.headers;
// Let fetch's header continuation settle while the server keeps the body open.
await new Promise(resolve => setImmediate(resolve));
}
test('a saved mutation whose response body stalls is bounded and is not retried', { timeout: 5000 }, async t => {
const url = await stalledResponse(t);
let requests = 0;
const api = loadClient((...args) => { requests++; return fetch(...args); });
const outcome = api.authenticatedJSON('PATCH', url, '', undefined, { preferred_name: 'synthetic' }).catch(error => error);
await waitForBody(api);
api.expire();
const error = await outcome;
assert.ok(error instanceof api.APIRequestError);
assert.equal(error.code, 'REQUEST_TIMEOUT');
assert.equal(error.status, 0);
assert.equal(error.message, '保存结果尚未确认,请先核验服务器记录');
assert.equal(requests, 1);
assert.deepEqual(api.delays, [15000]);
assert.equal(api.timers.size, 0);
assert.deepEqual(api.events, []);
});
test('GET body timeout retains read-only recovery guidance', { timeout: 5000 }, async t => {
const api = loadClient();
const outcome = api.authenticatedJSON('GET', await stalledResponse(t), '').catch(error => error);
await waitForBody(api);
api.expire();
const error = await outcome;
assert.equal(error.code, 'REQUEST_TIMEOUT');
assert.match(error.message, /请求超时/);
assert.doesNotMatch(error.message, /保存结果/);
});
test('a stalled HTTP error body is bounded without revoking authentication', { timeout: 5000 }, async t => {
const api = loadClient();
const outcome = api.authenticatedJSON('PATCH', await stalledResponse(t, 502), '').catch(error => error);
await waitForBody(api);
api.expire();
assert.equal((await outcome).code, 'REQUEST_TIMEOUT');
assert.deepEqual(api.events, []);
});
test('caller cancellation while reading an error body is not a timeout or auth failure', { timeout: 5000 }, async t => {
const api = loadClient();
const caller = new AbortController();
const outcome = api.authenticatedJSON('GET', await stalledResponse(t, 401), '', caller.signal).catch(error => error);
await waitForBody(api);
caller.abort();
const error = await outcome;
assert.equal(error.name, 'AbortError');
assert.equal(error.code, DOMException.ABORT_ERR);
assert.equal(api.timers.size, 0);
assert.deepEqual(api.events, []);
});
test('successful JSON responses clear their timers and preserve creation timeout', async () => {
const api = loadClient(async () => Response.json({ data: { id: 'synthetic' } }));
const result = await api.authenticatedJSON('POST', '/api/v1/subjects', '', undefined, { preferred_name: 'synthetic' });
assert.equal(result.data.id, 'synthetic');
assert.deepEqual(api.delays, [55000]);
assert.equal(api.timers.size, 0);
});
test('completed 502 HTML preserves the HTTP failure without authentication events', async () => {
const api = loadClient(async () => new Response('<h1>Bad Gateway</h1>', { status: 502 }));
const error = await api.authenticatedJSON('PATCH', '/api/v1/subjects/test', '').catch(value => value);
assert.ok(error instanceof api.APIRequestError);
assert.equal(error.status, 502);
assert.deepEqual(api.events, []);
assert.equal(api.timers.size, 0);
});
test('completed 401 JSON still requests the existing authentication check', async () => {
const api = loadClient(async () => Response.json({ error: { code: 'UNAUTHORIZED', message: 'synthetic' } }, { status: 401 }));
const error = await api.authenticatedJSON('GET', '/api/v1/me', '').catch(value => value);
assert.equal(error.status, 401);
assert.equal(error.code, 'UNAUTHORIZED');
assert.deepEqual(api.events, [api.AUTHENTICATION_REQUIRED_EVENT]);
});
test('invalid successful JSON remains an unconfirmed response rather than success', async () => {
const api = loadClient(async () => new Response('{', { status: 200 }));
const error = await api.authenticatedJSON('PATCH', '/api/v1/subjects/test', '').catch(value => value);
assert.equal(error.name, 'SyntaxError');
assert.equal(api.timers.size, 0);
assert.deepEqual(api.events, []);
});