Keep private device list requests separate across view generations
This commit is contained in:
@@ -87,3 +87,15 @@ Final follow-up receipts:
|
|||||||
|
|
||||||
All fixture browser/server processes stopped after verification. No node, helper,
|
All fixture browser/server processes stopped after verification. No node, helper,
|
||||||
firewall, wallet or saved-device configuration was changed by these UI checks.
|
firewall, wallet or saved-device configuration was changed by these UI checks.
|
||||||
|
|
||||||
|
A final narrow follow-up disables in-flight deduplication for the private device
|
||||||
|
list. The shared client's dedup key is only method+params; a new view/capability
|
||||||
|
generation could otherwise attach to the preceding generation's unresolved
|
||||||
|
promise. The device suite now passes 18/18, including fresh-list recovery and late
|
||||||
|
old-response rejection (`/tmp/archy-firewall-private-generation-tests-20261008.log`).
|
||||||
|
No broader browser rerun was needed for this request-policy-only change.
|
||||||
|
|
||||||
|
Retry behavior is unchanged: this client's maxRetries value of 1 already means
|
||||||
|
one network attempt, just like 0 after its minimum clamp. A confirmed pre-dispatch
|
||||||
|
CSRF rejection may still refresh credentials and retry. No cross-node dedup fault
|
||||||
|
was claimed: the client's base URL is fixed per instance.
|
||||||
|
|||||||
@@ -36,7 +36,23 @@ describe('Private device management', () => {
|
|||||||
expect(rpcClient.call).not.toHaveBeenCalled()
|
expect(rpcClient.call).not.toHaveBeenCalled()
|
||||||
await w.setProps({ statusKnown: true }); await flushPromises()
|
await w.setProps({ statusKnown: true }); await flushPromises()
|
||||||
expect(rpcClient.call).toHaveBeenCalledTimes(1)
|
expect(rpcClient.call).toHaveBeenCalledTimes(1)
|
||||||
expect(rpcClient.call).toHaveBeenCalledWith(expect.objectContaining({ method: 'vpn.list-peers' }))
|
expect(rpcClient.call).toHaveBeenCalledWith(expect.objectContaining({ method: 'vpn.list-peers', dedup: false }))
|
||||||
|
w.unmount()
|
||||||
|
})
|
||||||
|
it('starts a fresh non-deduplicated list after capability recovery and ignores the prior generation', async () => {
|
||||||
|
const replies: Array<(value: { peers: typeof peer[] }) => void> = []
|
||||||
|
vi.mocked(rpcClient.call).mockImplementation(async () => new Promise(resolve => { replies.push(resolve) }))
|
||||||
|
const w = view(); await flushPromises()
|
||||||
|
await w.setProps({ managementVerified: false }); await flushPromises()
|
||||||
|
await w.setProps({ managementVerified: true }); await flushPromises()
|
||||||
|
expect(replies).toHaveLength(2)
|
||||||
|
for (const [options] of vi.mocked(rpcClient.call).mock.calls) {
|
||||||
|
expect(options).toEqual(expect.objectContaining({ method: 'vpn.list-peers', dedup: false }))
|
||||||
|
}
|
||||||
|
replies[1]!({ peers: [{ ...peer, name: 'Tablet' }] }); await flushPromises()
|
||||||
|
expect(w.text()).toContain('Tablet')
|
||||||
|
replies[0]!({ peers: [{ ...peer, name: 'Old Phone' }] }); await flushPromises()
|
||||||
|
expect(w.text()).toContain('Tablet'); expect(w.text()).not.toContain('Old Phone')
|
||||||
w.unmount()
|
w.unmount()
|
||||||
})
|
})
|
||||||
it('clears private details and rejects an in-flight reveal when capability is lost', async () => {
|
it('clears private details and rejects an in-flight reveal when capability is lost', async () => {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ async function refresh() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
try {
|
try {
|
||||||
const result = await rpcClient.call<{ peers: Device[] }>({ method: 'vpn.list-peers', dedup: true, maxRetries: 1 })
|
const result = await rpcClient.call<{ peers: Device[] }>({ method: 'vpn.list-peers', dedup: false, maxRetries: 1 })
|
||||||
if (!canReadDevices.value || !active || generation !== request) return
|
if (!canReadDevices.value || !active || generation !== request) return
|
||||||
if (!Array.isArray(result?.peers) || result.peers.some(p => !p || typeof p.name !== 'string' || typeof p.ip !== 'string' || (p.management_state !== undefined && !['active', 'pending', 'revoking'].includes(p.management_state)))) throw new Error('Invalid device list')
|
if (!Array.isArray(result?.peers) || result.peers.some(p => !p || typeof p.name !== 'string' || typeof p.ip !== 'string' || (p.management_state !== undefined && !['active', 'pending', 'revoking'].includes(p.management_state)))) throw new Error('Invalid device list')
|
||||||
devices.value = result.peers.map(p => ({ name: p.name, ip: p.ip, management_state: p.management_state }))
|
devices.value = result.peers.map(p => ({ name: p.name, ip: p.ip, management_state: p.management_state }))
|
||||||
|
|||||||
Reference in New Issue
Block a user