





















@@ -180,6 +180,31 @@ function expectProbeAuthFields(
180180}
181181}
182182183+let probeUrlSeq = 0;
184+185+function nextProbeUrl(label: string): string {
186+probeUrlSeq += 1;
187+return `ws://127.0.0.1:18789/${label}-${probeUrlSeq}`;
188+}
189+190+function setDeviceRequiredProbeMode(): void {
191+deviceIdentityState.cachedToken = null;
192+gatewayClientState.startMode = "close";
193+gatewayClientState.close = { code: 1008, reason: "device identity required" };
194+}
195+196+function lastGatewayClientOptions(): Record<string, unknown> | null {
197+return gatewayClientState.options;
198+}
199+200+async function runLightweightProbe(url: string): Promise<Awaited<ReturnType<typeof probeGateway>>> {
201+return await probeGateway({
202+ url,
203+timeoutMs: 1_000,
204+includeDetails: false,
205+});
206+}
207+183208describe("probeGateway", () => {
184209beforeEach(() => {
185210deviceIdentityState.throwOnLoad = false;
@@ -536,4 +561,160 @@ describe("probeGateway", () => {
536561close: null,
537562});
538563});
564+565+it("short-circuits later unpaired probes after repeated device-required closes", async () => {
566+setDeviceRequiredProbeMode();
567+const url = nextProbeUrl("device-required");
568+569+for (let i = 0; i < 3; i += 1) {
570+gatewayClientState.options = null;
571+const result = await runLightweightProbe(url);
572+573+expectProbeResultFields(result, {
574+ok: false,
575+error: "gateway closed (1008): device identity required",
576+close: { code: 1008, reason: "device identity required" },
577+});
578+expect(lastGatewayClientOptions()?.url).toBe(url);
579+}
580+581+const startCalls = gatewayClientState.startCalls;
582+gatewayClientState.options = null;
583+584+const result = await runLightweightProbe(url);
585+586+expectProbeResultFields(result, {
587+ok: false,
588+connectLatencyMs: null,
589+error: "gateway closed (1008): device identity required",
590+close: {
591+code: 1008,
592+reason: "device identity required",
593+hint: "probe short-circuited by recent device-required rejections",
594+},
595+health: null,
596+status: null,
597+presence: null,
598+configSnapshot: null,
599+});
600+expectProbeAuthFields(result, {
601+role: null,
602+scopes: [],
603+capability: "unknown",
604+});
605+expect(gatewayClientState.startCalls).toBe(startCalls);
606+expect(lastGatewayClientOptions()).toBeNull();
607+});
608+609+it("does not cache other policy-close reasons", async () => {
610+deviceIdentityState.cachedToken = null;
611+gatewayClientState.startMode = "close";
612+gatewayClientState.close = { code: 1008, reason: "pairing required" };
613+const url = nextProbeUrl("pairing-required");
614+615+for (let i = 0; i < 4; i += 1) {
616+gatewayClientState.options = null;
617+const result = await runLightweightProbe(url);
618+619+expect(result.close).toEqual({ code: 1008, reason: "pairing required" });
620+expect(lastGatewayClientOptions()?.url).toBe(url);
621+}
622+});
623+624+it("keeps device-required probe cache entries per URL", async () => {
625+setDeviceRequiredProbeMode();
626+const firstUrl = nextProbeUrl("first-device-required");
627+const secondUrl = nextProbeUrl("second-device-required");
628+629+for (let i = 0; i < 3; i += 1) {
630+await runLightweightProbe(firstUrl);
631+}
632+633+gatewayClientState.options = null;
634+const result = await runLightweightProbe(secondUrl);
635+636+expect(result.close).toEqual({ code: 1008, reason: "device identity required" });
637+expect(result.close?.hint).toBeUndefined();
638+expect(lastGatewayClientOptions()?.url).toBe(secondUrl);
639+});
640+641+it("expires device-required probe cache entries after the TTL", async () => {
642+setDeviceRequiredProbeMode();
643+const url = nextProbeUrl("ttl-device-required");
644+let nowMs = 1_000_000;
645+const dateNowSpy = vi.spyOn(Date, "now").mockImplementation(() => nowMs);
646+try {
647+for (let i = 0; i < 3; i += 1) {
648+await runLightweightProbe(url);
649+}
650+651+nowMs += 5 * 60_000;
652+gatewayClientState.options = null;
653+const result = await runLightweightProbe(url);
654+655+expect(result.close).toEqual({ code: 1008, reason: "device identity required" });
656+expect(result.close?.hint).toBeUndefined();
657+expect(lastGatewayClientOptions()?.url).toBe(url);
658+} finally {
659+dateNowSpy.mockRestore();
660+}
661+});
662+663+it("lets paired probes clear prior device-required failures", async () => {
664+setDeviceRequiredProbeMode();
665+const url = nextProbeUrl("paired-device-required");
666+667+for (let i = 0; i < 3; i += 1) {
668+await runLightweightProbe(url);
669+}
670+671+deviceIdentityState.cachedToken = {
672+token: "cached-operator-token",
673+role: "operator",
674+scopes: ["operator.read"],
675+updatedAtMs: 1,
676+};
677+gatewayClientState.startMode = "hello";
678+gatewayClientState.options = null;
679+680+const success = await runLightweightProbe(url);
681+682+expect(success.ok).toBe(true);
683+expect(lastGatewayClientOptions()?.url).toBe(url);
684+expect(lastGatewayClientOptions()?.deviceIdentity).toEqual(deviceIdentityState.value);
685+686+setDeviceRequiredProbeMode();
687+gatewayClientState.options = null;
688+const afterSuccess = await runLightweightProbe(url);
689+690+expect(afterSuccess.close).toEqual({ code: 1008, reason: "device identity required" });
691+expect(afterSuccess.close?.hint).toBeUndefined();
692+expect(lastGatewayClientOptions()?.url).toBe(url);
693+});
694+695+it("does not short-circuit explicit-auth probes after unauthenticated failures", async () => {
696+setDeviceRequiredProbeMode();
697+const url = nextProbeUrl("explicit-auth-device-required");
698+699+for (let i = 0; i < 3; i += 1) {
700+await runLightweightProbe(url);
701+}
702+703+gatewayClientState.startMode = "hello";
704+gatewayClientState.helloAuth = {};
705+gatewayClientState.options = null;
706+707+const result = await probeGateway({
708+ url,
709+auth: { token: "explicit-token" },
710+timeoutMs: 1_000,
711+includeDetails: false,
712+});
713+714+expect(result.ok).toBe(true);
715+expectProbeAuthFields(result, { capability: "connected_no_operator_scope" });
716+expect(lastGatewayClientOptions()?.url).toBe(url);
717+expect(lastGatewayClientOptions()?.token).toBe("explicit-token");
718+expect(lastGatewayClientOptions()?.deviceIdentity).toBeNull();
719+});
539720});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。