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
|
tests as load-bearing boot guards, so they were left intact rather than risk an unvalidated
|
||||||
E2E change).
|
E2E change).
|
||||||
|
|
||||||
**Phase 3 — P1 backlog (2–4 weeks, interleave with feature work)**
|
**Phase 3 — P1 backlog** — ⏳ partial (PR: `test/phase3-p1-gaps`). Verification-first pass: like §4
|
||||||
- Approval×turn integration, TransportManager race, socket backoff, hostile webhook payloads,
|
P0, most P1 "untested" claims were **inaccurate against current `main`**. Verified state:
|
||||||
path_scope invariant, web3 tool wiring, threadSlice/accountsSlice, journey spec, Playwright
|
- [x] **`TransportManager.raceLanAndTunnel`** — GENUINE GAP (the sibling test only covered profile
|
||||||
approval mirror.
|
*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)**
|
**Phase 4 — new dimensions (ongoing)**
|
||||||
- proptest + cargo-fuzz targets; scoped cargo-mutants; jest-axe lane; migration fixture;
|
- proptest + cargo-fuzz targets; scoped cargo-mutants; jest-axe lane; migration fixture;
|
||||||
|
|||||||
Reference in New Issue
Block a user