[verified] refactor: harden operations and redesign device console

This commit is contained in:
chick
2026-07-15 16:33:16 +08:00
parent e04a16e817
commit 4977afdc20
24 changed files with 769 additions and 779 deletions
+14 -13
View File
@@ -38,12 +38,12 @@ test('proxy path rejects normalization and repeated-decoding structure changes',
test('FileConfigStore preserves constructor env across transactions and serializes real writes', async () => {
const { configPath, examplePath } = await files()
const store = await FileConfigStore.open({ configPath, examplePath, env: { HOST: 'env-host', PORT: '4321' } })
const store = await FileConfigStore.open({ configPath, examplePath, env: { HOST: 'localhost', PORT: '4321' } })
await Promise.all([
store.add({ id: 'a', url: 'http://a.test' }),
store.add({ id: 'b', url: 'http://b.test' }),
store.add({ id: 'a', url: 'http://192.0.2.21' }),
store.add({ id: 'b', url: 'http://192.0.2.22' }),
])
assert.equal(store.snapshot.server.host, 'env-host')
assert.equal(store.snapshot.server.host, 'localhost')
assert.equal(store.snapshot.server.port, 4321)
assert.deepEqual(JSON.parse(await readFile(configPath, 'utf8')).instances.map(x => x.id), ['a', 'b'])
})
@@ -59,24 +59,24 @@ test('atomicWriteJson replaces content, leaves no temp and creates POSIX 0600',
test('schema rejects invalid port and auth mode', () => {
for (const port of [0, 65536, 1.5, 'wat']) assert.throws(() => normalizeConfig({ server: { port }, instances: [] }, {}), /port/)
assert.throws(() => normalizeConfig({ instances: [{ id: 'x', url: 'http://x.test', auth: { mode: 'token' } }] }, {}), /auth.mode/)
assert.throws(() => normalizeConfig({ instances: [{ id: 'x', url: 'http://192.0.2.27', auth: { mode: 'token' } }] }, {}), /auth.mode/)
})
test('registry reconcile builds next map before publishing and closes staged clients on create failure', () => {
const old = { id: 'old', url: 'http://old.test', auth: { mode: 'none', password: '' } }
const old = { id: 'old', url: 'http://192.0.2.24', auth: { mode: 'none', password: '' } }
let stagedClosed = false
const registry = new ClientRegistry({ createClient: item => {
if (item.id === 'bad') throw new Error('boom')
return { instance: item, close() { if (item.id === 'staged') stagedClosed = true } }
} }).reconcile([old])
assert.throws(() => registry.reconcile([{ id: 'staged', url: 'http://staged.test' }, { id: 'bad', url: 'http://bad.test' }]), /boom/)
assert.throws(() => registry.reconcile([{ id: 'staged', url: 'http://192.0.2.26' }, { id: 'bad', url: 'http://192.0.2.23' }]), /boom/)
assert.equal(stagedClosed, true)
assert.equal(registry.get('old').instance.id, 'old')
assert.equal(registry.get('bad'), undefined)
})
test('temporary login credential is ephemeral, saved flag comes from store, logout clears it', async t => {
const { configPath, examplePath } = await files([{ id: 'one', url: 'http://sim.test' }])
const { configPath, examplePath } = await files([{ id: 'one', url: 'http://192.0.2.25' }])
const store = await FileConfigStore.open({ configPath, examplePath })
let credential
let cleared = false
@@ -92,7 +92,7 @@ test('temporary login credential is ephemeral, saved flag comes from store, logo
})
test('failed temporary login credential is not retained for later automatic attempts', async () => {
const instance = { id: 'x', url: 'http://x.test', auth: { mode: 'none', password: '' } }
const instance = { id: 'x', url: 'http://192.0.2.27', auth: { mode: 'none', password: '' } }
const bodies = []
const c = createSimAdminClient(instance, { fetchImpl: async (url, options = {}) => {
const pathname = new URL(url).pathname
@@ -109,14 +109,15 @@ test('failed temporary login credential is not retained for later automatic atte
})
test('proxy body/header contract handles JSON text buffer, gzip metadata and stable 415', async t => {
const { configPath, examplePath } = await files([{ id: 'one', url: 'http://sim.test/base' }])
const { configPath, examplePath } = await files([{ id: 'one', url: 'http://192.0.2.25/base' }])
const store = await FileConfigStore.open({ configPath, examplePath })
const calls = []
const registry = new ClientRegistry({ createClient: instance => client(instance, async (url, options) => {
calls.push({ url: String(url), options })
return new Response('decoded', { status: 503, headers: { 'content-encoding': 'gzip', 'content-length': '999', 'x-upstream': 'yes' } })
}) }).reconcile(store.snapshot.instances)
const app = await buildApp({ configStore: store, clientRegistry: registry, staticFiles: false }); t.after(() => app.close())
const testPolicy = { authorize: () => ({ allowed: true, dangerous: false }) }
const app = await buildApp({ configStore: store, clientRegistry: registry, staticFiles: false, proxyPolicy: testPolicy }); t.after(() => app.close())
const json = await app.inject({ method: 'POST', url: '/api/proxy/one/api/x?q=1', headers: { 'content-type': 'application/json', 'accept-encoding': 'gzip' }, payload: { x: 1 } })
assert.equal(json.statusCode, 503); assert.equal(calls[0].options.body, '{"x":1}'); assert.equal(new URL(calls[0].url).pathname, '/api/x'); assert.equal(new URL(calls[0].url).search, '?q=1')
assert.equal(calls[0].options.headers.has('accept-encoding'), false); assert.equal(json.headers['content-encoding'], undefined); assert.equal(json.headers['content-length'], '7')
@@ -132,7 +133,7 @@ test('committed config survives reconcile failure and readiness degrades then re
let fail = true
const registry = new ClientRegistry({ createClient: instance => { if (fail) throw new Error('factory'); return client(instance, async () => new Response('{}')) } })
const app = await buildApp({ configStore: store, clientRegistry: registry, staticFiles: false, reconcileRetryMs: 10 }); t.after(() => app.close())
const created = await app.inject({ method: 'POST', url: '/api/instances', payload: { id: 'x', url: 'http://x.test' } })
const created = await app.inject({ method: 'POST', url: '/api/instances', payload: { id: 'x', url: 'http://192.0.2.27' } })
assert.equal(created.statusCode, 200); assert.equal(created.json().reconcilePending, true); assert.equal((await app.inject('/api/ready')).json().ready, false)
fail = false
await new Promise(resolve => setTimeout(resolve, 30))
@@ -141,7 +142,7 @@ test('committed config survives reconcile failure and readiness degrades then re
test('status supports registration_status and health 404 with successful business endpoint', async () => {
const values = new Map([['/api/health', { ok: false, status: 404, latencyMs: 2, data: null }], ['/api/auth/status', { ok: false, status: 404, data: null }], ['/api/device', { ok: true, status: 200, data: { model: 'X' } }], ['/api/network', { ok: true, status: 200, data: { registration_status: 'registered' } }]])
const instance = { id: 'x', name: 'x', url: 'http://x.test', auth: { mode: 'none', password: '' } }
const instance = { id: 'x', name: 'x', url: 'http://192.0.2.27', auth: { mode: 'none', password: '' } }
const c = createSimAdminClient(instance, { fetchImpl: async url => { const v = values.get(new URL(url).pathname) || { ok: false, status: 404, data: null }; return new Response(JSON.stringify(v.data), { status: v.status }) } })
const status = await collectInstanceStatus(c)
assert.equal(status.reachable, true); assert.notEqual(status.authenticated, false); assert.equal(status.summary.network.registration, 'registered')