






















@@ -1643,37 +1643,183 @@ describe("startGatewayConfigReloader watcher error recovery", () => {
16431643await reloader.stop();
16441644});
164516451646-it("disables hot-reload and logs at error level after the retry budget is exhausted", async () => {
1647-const watchers = [
1648-createWatcherMock(),
1649-createWatcherMock(),
1650-createWatcherMock(),
1651-createWatcherMock(),
1652-];
1653-const { watchSpy, log, reloader } = startReloaderWithWatchers(watchers);
1654-1655-// Three errors consume the retry budget; the fourth error escalates.
1656-watchers[0]?.emit("error");
1657-await vi.advanceTimersByTimeAsync(500);
1658-watchers[1]?.emit("error");
1659-await vi.advanceTimersByTimeAsync(2000);
1660-watchers[2]?.emit("error");
1661-await vi.advanceTimersByTimeAsync(5000);
1662-expect(watchSpy).toHaveBeenCalledTimes(4);
1663-expect(reloader.hotReloadStatus()).toBe("active");
1646+it("degrades to polling then disables after both native and polling retries are exhausted", async () => {
1647+const originalVitest = process.env.VITEST;
1648+const originalChokidarPolling = process.env.CHOKIDAR_USEPOLLING;
1649+delete process.env.VITEST;
1650+delete process.env.CHOKIDAR_USEPOLLING;
1651+let reloader: { stop: () => Promise<void>; hotReloadStatus: () => string } | undefined;
1652+try {
1653+// Native phase: initial watcher + 3 re-creates = 4 watchers.
1654+// Polling phase: 1 polling re-create + 3 re-creates = 4 watchers.
1655+const watchers = Array.from({ length: 8 }, () => createWatcherMock());
1656+const started = startReloaderWithWatchers(watchers);
1657+const { watchSpy, log } = started;
1658+reloader = started.reloader;
1659+const watchOptions = (index: number) =>
1660+watchSpy.mock.calls[index]?.[1] as { usePolling?: boolean } | undefined;
1661+1662+// --- Native retry phase (3 retries) ---
1663+expect(watchOptions(0)?.usePolling).toBe(false);
1664+watchers[0]?.emit("error");
1665+await vi.advanceTimersByTimeAsync(500);
1666+expect(watchOptions(1)?.usePolling).toBe(false);
1667+watchers[1]?.emit("error");
1668+await vi.advanceTimersByTimeAsync(2000);
1669+expect(watchOptions(2)?.usePolling).toBe(false);
1670+watchers[2]?.emit("error");
1671+await vi.advanceTimersByTimeAsync(5000);
1672+expect(watchSpy).toHaveBeenCalledTimes(4);
1673+expect(watchOptions(3)?.usePolling).toBe(false);
1674+expect(reloader.hotReloadStatus()).toBe("active");
1675+1676+// Fourth native error triggers degradation to polling mode (not disabled).
1677+watchers[3]?.emit("error");
1678+expect(reloader.hotReloadStatus()).toBe("active");
1679+expect(log.warn).toHaveBeenCalledWith(
1680+expect.stringContaining("degrading to polling mode"),
1681+);
1682+await vi.advanceTimersByTimeAsync(500);
1683+expect(watchSpy).toHaveBeenCalledTimes(5);
1684+expect(watchOptions(4)?.usePolling).toBe(true);
1685+1686+// --- Polling retry phase (3 retries) ---
1687+watchers[4]?.emit("error");
1688+await vi.advanceTimersByTimeAsync(500);
1689+expect(watchOptions(5)?.usePolling).toBe(true);
1690+watchers[5]?.emit("error");
1691+await vi.advanceTimersByTimeAsync(2000);
1692+expect(watchOptions(6)?.usePolling).toBe(true);
1693+watchers[6]?.emit("error");
1694+await vi.advanceTimersByTimeAsync(5000);
1695+expect(watchSpy).toHaveBeenCalledTimes(8);
1696+expect(watchOptions(7)?.usePolling).toBe(true);
1697+expect(reloader.hotReloadStatus()).toBe("active");
1698+1699+// Eighth error in polling mode finally disables hot-reload.
1700+watchers[7]?.emit("error");
1701+expect(reloader.hotReloadStatus()).toBe("disabled");
1702+expect(log.error).toHaveBeenCalledWith(
1703+expect.stringContaining(
1704+"config hot-reload disabled: watcher failed after 3 re-create attempts in polling mode",
1705+),
1706+);
1707+// No further watcher is created once disabled.
1708+await vi.advanceTimersByTimeAsync(10000);
1709+expect(watchSpy).toHaveBeenCalledTimes(8);
1710+} finally {
1711+if (originalVitest === undefined) {
1712+delete process.env.VITEST;
1713+} else {
1714+process.env.VITEST = originalVitest;
1715+}
1716+if (originalChokidarPolling === undefined) {
1717+delete process.env.CHOKIDAR_USEPOLLING;
1718+} else {
1719+process.env.CHOKIDAR_USEPOLLING = originalChokidarPolling;
1720+}
1721+await reloader?.stop();
1722+}
1723+});
166417241665-watchers[3]?.emit("error");
1666-expect(reloader.hotReloadStatus()).toBe("disabled");
1667-expect(log.error).toHaveBeenCalledWith(
1668-expect.stringContaining(
1669-"config hot-reload disabled: watcher failed after 3 re-create attempts",
1670-),
1671-);
1672-// No further watcher is created once disabled.
1673-await vi.advanceTimersByTimeAsync(10000);
1674-expect(watchSpy).toHaveBeenCalledTimes(4);
1725+it("does not run a redundant native phase when chokidar polling is forced on", async () => {
1726+const originalVitest = process.env.VITEST;
1727+const originalChokidarPolling = process.env.CHOKIDAR_USEPOLLING;
1728+delete process.env.VITEST;
1729+process.env.CHOKIDAR_USEPOLLING = "1";
1730+let reloader: { stop: () => Promise<void>; hotReloadStatus: () => string } | undefined;
1731+try {
1732+const watchers = Array.from({ length: 4 }, () => createWatcherMock());
1733+const started = startReloaderWithWatchers(watchers);
1734+const { watchSpy, log } = started;
1735+reloader = started.reloader;
1736+const watchOptions = (index: number) =>
1737+watchSpy.mock.calls[index]?.[1] as { usePolling?: boolean } | undefined;
1738+1739+expect(watchOptions(0)?.usePolling).toBe(true);
1740+watchers[0]?.emit("error");
1741+await vi.advanceTimersByTimeAsync(500);
1742+expect(watchOptions(1)?.usePolling).toBe(true);
1743+watchers[1]?.emit("error");
1744+await vi.advanceTimersByTimeAsync(2000);
1745+expect(watchOptions(2)?.usePolling).toBe(true);
1746+watchers[2]?.emit("error");
1747+await vi.advanceTimersByTimeAsync(5000);
1748+expect(watchOptions(3)?.usePolling).toBe(true);
1749+1750+watchers[3]?.emit("error");
1751+expect(reloader.hotReloadStatus()).toBe("disabled");
1752+expect(log.warn).not.toHaveBeenCalledWith(
1753+expect.stringContaining("degrading to polling mode"),
1754+);
1755+expect(log.error).toHaveBeenCalledWith(
1756+expect.stringContaining(
1757+"config hot-reload disabled: watcher failed after 3 re-create attempts in polling mode",
1758+),
1759+);
1760+} finally {
1761+if (originalVitest === undefined) {
1762+delete process.env.VITEST;
1763+} else {
1764+process.env.VITEST = originalVitest;
1765+}
1766+if (originalChokidarPolling === undefined) {
1767+delete process.env.CHOKIDAR_USEPOLLING;
1768+} else {
1769+process.env.CHOKIDAR_USEPOLLING = originalChokidarPolling;
1770+}
1771+await reloader?.stop();
1772+}
1773+});
167517741676-await reloader.stop();
1775+it("does not report polling fallback when chokidar polling is forced off", async () => {
1776+const originalVitest = process.env.VITEST;
1777+const originalChokidarPolling = process.env.CHOKIDAR_USEPOLLING;
1778+delete process.env.VITEST;
1779+process.env.CHOKIDAR_USEPOLLING = "0";
1780+let reloader: { stop: () => Promise<void>; hotReloadStatus: () => string } | undefined;
1781+try {
1782+const watchers = Array.from({ length: 4 }, () => createWatcherMock());
1783+const started = startReloaderWithWatchers(watchers);
1784+const { watchSpy, log } = started;
1785+reloader = started.reloader;
1786+const watchOptions = (index: number) =>
1787+watchSpy.mock.calls[index]?.[1] as { usePolling?: boolean } | undefined;
1788+1789+expect(watchOptions(0)?.usePolling).toBe(false);
1790+watchers[0]?.emit("error");
1791+await vi.advanceTimersByTimeAsync(500);
1792+expect(watchOptions(1)?.usePolling).toBe(false);
1793+watchers[1]?.emit("error");
1794+await vi.advanceTimersByTimeAsync(2000);
1795+expect(watchOptions(2)?.usePolling).toBe(false);
1796+watchers[2]?.emit("error");
1797+await vi.advanceTimersByTimeAsync(5000);
1798+expect(watchOptions(3)?.usePolling).toBe(false);
1799+1800+watchers[3]?.emit("error");
1801+expect(reloader.hotReloadStatus()).toBe("disabled");
1802+expect(log.warn).not.toHaveBeenCalledWith(
1803+expect.stringContaining("degrading to polling mode"),
1804+);
1805+expect(log.error).toHaveBeenCalledWith(
1806+expect.stringContaining(
1807+"config hot-reload disabled: watcher failed after 3 re-create attempts in native mode",
1808+),
1809+);
1810+} finally {
1811+if (originalVitest === undefined) {
1812+delete process.env.VITEST;
1813+} else {
1814+process.env.VITEST = originalVitest;
1815+}
1816+if (originalChokidarPolling === undefined) {
1817+delete process.env.CHOKIDAR_USEPOLLING;
1818+} else {
1819+process.env.CHOKIDAR_USEPOLLING = originalChokidarPolling;
1820+}
1821+await reloader?.stop();
1822+}
16771823});
16781824});
16791825此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。