mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
test: Phase 3 P1 — TransportManager LAN/Tunnel race (verified backlog, gap filled) (#4500)
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* LAN-vs-Tunnel race semantics for TransportManager.raceLanAndTunnel (plan.md
|
||||
* §4 P1). The sibling TransportManager.test.ts covers profile *selection* but
|
||||
* never exercises the race path, despite the docstring describing it. LAN and
|
||||
* Tunnel transports are mocked here so each scenario controls who is healthy
|
||||
* (and when), and asserts the winner + that the loser is closed.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ConnectionProfile } from './profileStore';
|
||||
import { TransportManager } from './TransportManager';
|
||||
|
||||
// Mutable health/close hooks the mocked transports read at call time. Declared
|
||||
// via vi.hoisted so the vi.mock factories (hoisted above imports) can see them.
|
||||
const h = vi.hoisted(() => ({
|
||||
lanHealthy: (): Promise<boolean> => Promise.resolve(false),
|
||||
tunnelHealthy: (): Promise<boolean> => Promise.resolve(false),
|
||||
lanClose: vi.fn((): Promise<void> => Promise.resolve()),
|
||||
tunnelClose: vi.fn((): Promise<void> => Promise.resolve()),
|
||||
}));
|
||||
|
||||
vi.mock('./LanHttpTransport', () => ({
|
||||
LanHttpTransport: class {
|
||||
readonly kind = 'lan-http';
|
||||
isHealthy() {
|
||||
return h.lanHealthy();
|
||||
}
|
||||
close() {
|
||||
return h.lanClose();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('./TunnelTransport', () => ({
|
||||
TunnelTransport: class {
|
||||
readonly kind = 'tunnel';
|
||||
isHealthy() {
|
||||
return h.tunnelHealthy();
|
||||
}
|
||||
close() {
|
||||
return h.tunnelClose();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
function tunnelProfile(overrides: Partial<ConnectionProfile> = {}): ConnectionProfile {
|
||||
return {
|
||||
id: 'race-profile',
|
||||
label: 'Race',
|
||||
kind: 'tunnel',
|
||||
// rpcUrl present → raceLanAndTunnel actually races (not tunnel-only).
|
||||
rpcUrl: 'http://192.168.1.5:7788/rpc',
|
||||
channelId: 'CHANNEL001',
|
||||
corePubkey: 'dGVzdHB1YmtleXRlc3RwdWJrZXl0ZXN0cHVia2V5',
|
||||
sessionToken: 'tok123',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function manager(profile: ConnectionProfile): TransportManager {
|
||||
return new TransportManager(
|
||||
profile,
|
||||
() => Promise.resolve(''),
|
||||
() => Promise.resolve(null),
|
||||
'http://backend:3000'
|
||||
);
|
||||
}
|
||||
|
||||
const immediately = (v: boolean) => () => Promise.resolve(v);
|
||||
const after = (ms: number, v: boolean) => () =>
|
||||
new Promise<boolean>(resolve => setTimeout(() => resolve(v), ms));
|
||||
|
||||
describe('TransportManager.raceLanAndTunnel', () => {
|
||||
beforeEach(() => {
|
||||
h.lanHealthy = immediately(false);
|
||||
h.tunnelHealthy = immediately(false);
|
||||
h.lanClose.mockClear();
|
||||
h.tunnelClose.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('LAN wins the race and the tunnel is closed', async () => {
|
||||
h.lanHealthy = immediately(true);
|
||||
h.tunnelHealthy = after(50, true);
|
||||
|
||||
const t = await manager(tunnelProfile()).getTransport();
|
||||
|
||||
expect(t.kind).toBe('lan-http');
|
||||
expect(h.tunnelClose).toHaveBeenCalledTimes(1);
|
||||
expect(h.lanClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('tunnel wins the race and the LAN transport is closed', async () => {
|
||||
h.tunnelHealthy = immediately(true);
|
||||
h.lanHealthy = after(50, true);
|
||||
|
||||
const t = await manager(tunnelProfile()).getTransport();
|
||||
|
||||
expect(t.kind).toBe('tunnel');
|
||||
expect(h.lanClose).toHaveBeenCalledTimes(1);
|
||||
expect(h.tunnelClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws when neither transport becomes healthy', async () => {
|
||||
h.lanHealthy = immediately(false);
|
||||
h.tunnelHealthy = immediately(false);
|
||||
|
||||
await expect(manager(tunnelProfile()).getTransport()).rejects.toThrow(/all transports failed/);
|
||||
});
|
||||
|
||||
it('reset() re-races: LAN wins first, then after reset the tunnel wins', async () => {
|
||||
const mgr = manager(tunnelProfile());
|
||||
|
||||
// First race: LAN wins.
|
||||
h.lanHealthy = immediately(true);
|
||||
h.tunnelHealthy = after(50, false);
|
||||
const first = await mgr.getTransport();
|
||||
expect(first.kind).toBe('lan-http');
|
||||
|
||||
// LAN later fails → caller resets, and the next race must pick the tunnel
|
||||
// (tunnel healthy immediately; LAN's slow-unhealthy result loses the race).
|
||||
await mgr.reset();
|
||||
h.lanHealthy = after(50, false);
|
||||
h.tunnelHealthy = immediately(true);
|
||||
const second = await mgr.getTransport();
|
||||
expect(second.kind).toBe('tunnel');
|
||||
});
|
||||
|
||||
it('caches the winner: a second getTransport does not re-race', async () => {
|
||||
h.lanHealthy = immediately(true);
|
||||
h.tunnelHealthy = immediately(false);
|
||||
const mgr = manager(tunnelProfile());
|
||||
|
||||
const a = await mgr.getTransport();
|
||||
const b = await mgr.getTransport();
|
||||
|
||||
expect(a).toBe(b);
|
||||
// Tunnel closed exactly once (from the single race), not twice.
|
||||
expect(h.tunnelClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -414,10 +414,25 @@ of the five "ZERO tests" claims turned out **already covered**, so only the genu
|
||||
tests as load-bearing boot guards, so they were left intact rather than risk an unvalidated
|
||||
E2E change).
|
||||
|
||||
**Phase 3 — P1 backlog (2–4 weeks, interleave with feature work)**
|
||||
- Approval×turn integration, TransportManager race, socket backoff, hostile webhook payloads,
|
||||
path_scope invariant, web3 tool wiring, threadSlice/accountsSlice, journey spec, Playwright
|
||||
approval mirror.
|
||||
**Phase 3 — P1 backlog** — ⏳ partial (PR: `test/phase3-p1-gaps`). Verification-first pass: like §4
|
||||
P0, most P1 "untested" claims were **inaccurate against current `main`**. Verified state:
|
||||
- [x] **`TransportManager.raceLanAndTunnel`** — GENUINE GAP (the sibling test only covered profile
|
||||
*selection*, never the race). Added a dedicated `TransportManager.race.test.ts`: LAN-wins /
|
||||
tunnel-wins (loser closed), both-fail throws, `reset()` re-race, and winner-caching. Surfaced a
|
||||
real timing quirk: when the first `isHealthy()` to settle is `false`, the
|
||||
`Promise.race`→`Promise.any` fallback grabs that already-fulfilled `null` and throws instead of
|
||||
waiting for a later-healthy peer — worth a production follow-up.
|
||||
- [~] ALREADY COVERED (no gap): `composerInteractionBlocked` (ChatComposer + composerSendDecision
|
||||
tests), `threadSlice`/`accountsSlice` (multiple `__tests__`), CustomInference/CustomSearch
|
||||
onboarding pages, AgentAccessPanel (23 tests), and every P2 slice reducer
|
||||
(socket/channelConnections/ptt/providerSurface) + TeamInvites. web3 tool layer has `web3_tests.rs`.
|
||||
- [~] NOT A GAP: primary socket-client backoff is delegated to socket.io
|
||||
(`reconnectionAttempts: Infinity`), not custom logic — nothing bespoke to unit-test.
|
||||
- [ ] Genuine-but-deferred (need a full Rust build + deep store/parser fixtures, high iteration cost
|
||||
in the authoring env): memory two-source-one-tree `path_scope` invariant; broad hostile-webhook
|
||||
payload matrix (wrong types / deep nesting / oversized) beyond the current missing-field tests;
|
||||
the ~20 controller domains with no `tests/` reference (the Phase 0 inventory allowlist to burn
|
||||
down). Approval×turn integration, journey spec, and the Playwright approval mirror need WDIO/PW.
|
||||
|
||||
**Phase 4 — new dimensions (ongoing)**
|
||||
- proptest + cargo-fuzz targets; scoped cargo-mutants; jest-axe lane; migration fixture;
|
||||
|
||||
Reference in New Issue
Block a user