refactor(gateway): consolidate chat voice and reload test fixtures (#114420)

This commit is contained in:
Peter Steinberger
2026-07-27 04:38:00 -04:00
committed by GitHub
parent 13bbcabeab
commit 4029070c3d
4 changed files with 816 additions and 1861 deletions
+61 -114
View File
@@ -598,6 +598,10 @@ function createWatcherMock(effectiveUsePolling?: boolean) {
return watcher;
}
function makeGatewayPortConfig(port: number): OpenClawConfig {
return { gateway: { reload: {}, port } };
}
function makeSnapshot(partial: Partial<ConfigFileSnapshot> = {}): ConfigFileSnapshot {
const config = partial.config ?? {};
const sourceConfig = (partial.sourceConfig ??
@@ -827,6 +831,11 @@ function createReloaderHarness(
type ReloaderHarness = ReturnType<typeof createReloaderHarness>;
async function flushWatcherChange(harness: ReloaderHarness) {
harness.watcher.emit("change");
await vi.runAllTimersAsync();
}
function getOnlyRestartCall(harness: ReloaderHarness): [GatewayReloadPlan, OpenClawConfig] {
expect(harness.onRestart).toHaveBeenCalledTimes(1);
const call = harness.onRestart.mock.calls[0];
@@ -976,12 +985,8 @@ describe("startGatewayConfigReloader", () => {
});
it("watches resolved includes and reconciles them after an accepted reload", async () => {
const initialConfig: OpenClawConfig = {
gateway: { reload: {}, port: 18789 },
};
const nextConfig: OpenClawConfig = {
gateway: { reload: {}, port: 18790 },
};
const initialConfig = makeGatewayPortConfig(18789);
const nextConfig = makeGatewayPortConfig(18790);
const initialIncludePath = "/tmp/initial.json5";
const retainedIncludePath = "/tmp/retained.json5";
const addedIncludePath = "/tmp/added.json5";
@@ -1005,8 +1010,7 @@ describe("startGatewayConfigReloader", () => {
expect.objectContaining({ ignoreInitial: true }),
);
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
// Candidate discovery adds the new include before acceptance; acceptance
// then retires the old include in a second readiness-reconciled watcher.
@@ -1044,16 +1048,14 @@ describe("startGatewayConfigReloader", () => {
initialIncludedPaths: [acceptedIncludePath],
});
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.watcher.close).toHaveBeenCalledOnce();
expect(chokidar.watch).toHaveBeenLastCalledWith(
["/tmp/openclaw.json", acceptedIncludePath, firstCandidatePath],
expect.objectContaining({ ignoreInitial: true }),
);
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.watcher.close).toHaveBeenCalledTimes(2);
expect(chokidar.watch).toHaveBeenLastCalledWith(
["/tmp/openclaw.json", acceptedIncludePath, secondCandidatePath],
@@ -1092,19 +1094,14 @@ describe("startGatewayConfigReloader", () => {
});
it("journals valid external watcher edits and advances the snapshot slot", async () => {
const initialConfig: OpenClawConfig = {
gateway: { reload: {}, port: 18789 },
};
const nextConfig: OpenClawConfig = {
gateway: { reload: {}, port: 18790 },
};
const initialConfig = makeGatewayPortConfig(18789);
const nextConfig = makeGatewayPortConfig(18790);
const readSnapshot = vi.fn(async () =>
makeSnapshot({ config: nextConfig, parsed: nextConfig, hash: "next-raw-hash" }),
);
const harness = createReloaderHarness(readSnapshot, { initialConfig });
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(configAuditMocks.append).toHaveBeenCalledOnce();
expect(configAuditMocks.append.mock.calls[0]?.[0]?.record).toMatchObject({
@@ -1127,12 +1124,8 @@ describe("startGatewayConfigReloader", () => {
});
it("does not duplicate another OpenClaw process's journaled write", async () => {
const initialConfig: OpenClawConfig = {
gateway: { reload: {}, port: 18789 },
};
const nextConfig: OpenClawConfig = {
gateway: { reload: {}, port: 18790 },
};
const initialConfig = makeGatewayPortConfig(18789);
const nextConfig = makeGatewayPortConfig(18790);
const harness = createReloaderHarness(
vi.fn(async () =>
makeSnapshot({ config: nextConfig, parsed: nextConfig, hash: "other-write" }),
@@ -1146,8 +1139,7 @@ describe("startGatewayConfigReloader", () => {
});
configAuditMocks.append.mockClear();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(configAuditMocks.append).not.toHaveBeenCalled();
expect(configAuditMocks.upsertSnapshot).toHaveBeenLastCalledWith(
@@ -1173,8 +1165,7 @@ describe("startGatewayConfigReloader", () => {
);
configAuditMocks.upsertSnapshot.mockClear();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(configAuditMocks.append.mock.calls[0]?.[0]?.record).toMatchObject({
event: "config.external",
@@ -1203,15 +1194,12 @@ describe("startGatewayConfigReloader", () => {
const harness = createReloaderHarness(vi.fn(async () => activeSnapshot));
configAuditMocks.append.mockClear();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
await flushWatcherChange(harness);
expect(configAuditMocks.append).toHaveBeenCalledOnce();
activeSnapshot = secondInvalid;
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(configAuditMocks.append).toHaveBeenCalledTimes(2);
expect(configAuditMocks.append.mock.calls[1]?.[0]?.record).toMatchObject({
@@ -1224,17 +1212,13 @@ describe("startGatewayConfigReloader", () => {
});
it("uses the last observed hash when a valid edit follows an invalid one", async () => {
const initialConfig: OpenClawConfig = {
gateway: { reload: {}, port: 18789 },
};
const initialConfig = makeGatewayPortConfig(18789);
const invalid = makeSnapshot({
valid: false,
hash: "invalid-raw-hash",
issues: [{ path: "gateway.port", message: "expected number" }],
});
const nextConfig: OpenClawConfig = {
gateway: { reload: {}, port: 18790 },
};
const nextConfig = makeGatewayPortConfig(18790);
let activeSnapshot = invalid;
const harness = createReloaderHarness(
vi.fn(async () => activeSnapshot),
@@ -1242,15 +1226,13 @@ describe("startGatewayConfigReloader", () => {
);
configAuditMocks.append.mockClear();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
activeSnapshot = makeSnapshot({
config: nextConfig,
parsed: nextConfig,
hash: "valid-raw-hash",
});
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(configAuditMocks.append.mock.calls[1]?.[0]?.record).toMatchObject({
detectedBy: "watch",
@@ -1263,9 +1245,7 @@ describe("startGatewayConfigReloader", () => {
});
it("journals a return to the accepted bytes after an invalid edit", async () => {
const initialConfig: OpenClawConfig = {
gateway: { reload: {}, port: 18789 },
};
const initialConfig = makeGatewayPortConfig(18789);
const invalid = makeSnapshot({
valid: false,
hash: "invalid-raw-hash",
@@ -1278,15 +1258,13 @@ describe("startGatewayConfigReloader", () => {
);
configAuditMocks.append.mockClear();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
activeSnapshot = makeSnapshot({
config: initialConfig,
parsed: initialConfig,
hash: "initial-raw-hash",
});
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(configAuditMocks.append.mock.calls[1]?.[0]?.record).toMatchObject({
detectedBy: "watch",
@@ -1323,8 +1301,7 @@ describe("startGatewayConfigReloader", () => {
);
configAuditMocks.append.mockClear();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(configAuditMocks.append.mock.calls[0]?.[0]?.record).toMatchObject({
detectedBy: "watch",
@@ -1520,8 +1497,7 @@ describe("startGatewayConfigReloader", () => {
);
configAuditMocks.append.mockClear();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(configAuditMocks.append).not.toHaveBeenCalled();
await harness.reloader.stop();
@@ -1545,8 +1521,7 @@ describe("startGatewayConfigReloader", () => {
);
configAuditMocks.append.mockClear();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(configAuditMocks.append.mock.calls[0]?.[0]?.record).toMatchObject({
event: "config.external",
@@ -1603,8 +1578,7 @@ describe("startGatewayConfigReloader", () => {
);
const harness = createReloaderHarness(readSnapshot, { initialConfig });
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onConfigCandidateCommitted).toHaveBeenCalledOnce();
expect(harness.onConfigCandidateCommitted).toHaveBeenCalledWith({
@@ -1615,8 +1589,7 @@ describe("startGatewayConfigReloader", () => {
// A same-content echo must not re-notify: nothing changed.
harness.onConfigCandidateCommitted.mockClear();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onConfigCandidateCommitted).not.toHaveBeenCalled();
await harness.reloader.stop();
});
@@ -1634,8 +1607,7 @@ describe("startGatewayConfigReloader", () => {
);
const harness = createReloaderHarness(readSnapshot, { initialConfig });
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onHotReload).not.toHaveBeenCalled();
expect(harness.onRestart).not.toHaveBeenCalled();
@@ -1644,16 +1616,13 @@ describe("startGatewayConfigReloader", () => {
});
it("notifies lifecycle owners when a persisted edit reverts to the current baseline", async () => {
const initialConfig: OpenClawConfig = {
gateway: { reload: {}, port: 18789 },
};
const initialConfig = makeGatewayPortConfig(18789);
const readSnapshot = vi.fn(async () =>
makeSnapshot({ config: initialConfig, hash: "reverted-restart-edit" }),
);
const harness = createReloaderHarness(readSnapshot, { initialConfig });
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onConfigAccepted).toHaveBeenCalledOnce();
expect(harness.onConfigApplied).not.toHaveBeenCalled();
@@ -1679,8 +1648,7 @@ describe("startGatewayConfigReloader", () => {
onConfigCandidateObserved,
});
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(onConfigCandidateObserved).toHaveBeenCalledOnce();
expect(harness.onConfigAccepted).toHaveBeenCalledOnce();
@@ -1743,8 +1711,7 @@ describe("startGatewayConfigReloader", () => {
await vi.runAllTimersAsync();
harness.onConfigAccepted.mockClear();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(onRestart).toHaveBeenCalledOnce();
expect(onRestart.mock.calls[0]?.[3]).toEqual(effectiveConfig);
@@ -1962,8 +1929,7 @@ describe("startGatewayConfigReloader", () => {
initialInternalWriteHash: "accepted-write",
});
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onConfigAccepted).not.toHaveBeenCalled();
expect(harness.onRestart).not.toHaveBeenCalled();
@@ -2212,8 +2178,7 @@ describe("startGatewayConfigReloader", () => {
sourceFingerprint: "source-queued",
writtenAtMs: Date.now(),
});
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(readSnapshot).toHaveBeenCalledTimes(1);
expect(harness.onNoopConfigCommit).not.toHaveBeenCalled();
@@ -2500,8 +2465,7 @@ describe("startGatewayConfigReloader", () => {
const readSnapshot = vi.fn(async () => makeSnapshot({ config: nextConfig, hash: "sandbox" }));
const harness = createReloaderHarness(readSnapshot, { initialConfig });
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onConfigChange).toHaveBeenCalledTimes(1);
expect(harness.onConfigChange.mock.calls[0]?.[0].noopPaths).toContain(
@@ -2528,8 +2492,7 @@ describe("startGatewayConfigReloader", () => {
);
const harness = createReloaderHarness(readSnapshot, { initialConfig });
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onNoopConfigCommit).toHaveBeenCalledTimes(1);
expect(harness.onNoopConfigCommit.mock.calls[0]?.[0].noopPaths).toContain(
@@ -2582,8 +2545,7 @@ describe("startGatewayConfigReloader", () => {
pinActivePluginChannelRegistry(channelRegistry);
try {
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
const [plan] = getOnlyHotReloadCall(harness);
expect(plan.restartChannelAccounts).toEqual(new Map([["mattermost", new Set(["alpha"])]]));
@@ -2677,8 +2639,7 @@ describe("startGatewayConfigReloader", () => {
const readSnapshot = vi.fn(async () => makeSnapshot({ config: nextConfig, hash: "hot" }));
const harness = createReloaderHarness(readSnapshot, { initialConfig });
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onConfigChange.mock.invocationCallOrder[0]).toBeLessThan(
harness.onHotReload.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY,
@@ -2702,8 +2663,7 @@ describe("startGatewayConfigReloader", () => {
const readSnapshot = vi.fn(async () => makeSnapshot({ config: nextConfig, hash: "terminal" }));
const harness = createReloaderHarness(readSnapshot, { initialConfig });
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
await Promise.resolve();
expect(harness.onConfigChange).toHaveBeenCalledTimes(1);
@@ -2763,8 +2723,7 @@ describe("startGatewayConfigReloader", () => {
const readSnapshot = vi.fn(async () => makeSnapshot({ config: nextConfig, hash: "off" }));
const harness = createReloaderHarness(readSnapshot, { initialConfig });
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onConfigChange).not.toHaveBeenCalled();
expect(harness.onHotReload).not.toHaveBeenCalled();
@@ -2782,8 +2741,7 @@ describe("startGatewayConfigReloader", () => {
const readSnapshot = vi.fn(async () => makeSnapshot({ config: nextConfig, hash: "hot" }));
const harness = createReloaderHarness(readSnapshot, { initialConfig });
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onConfigChange).toHaveBeenCalledOnce();
expect(harness.onHotReload).not.toHaveBeenCalled();
@@ -3083,8 +3041,7 @@ describe("startGatewayConfigReloader", () => {
harness.emitWrite(makeZeroDebounceHookWrite("internal-retry-1"));
await vi.runAllTimersAsync();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onHotReload).toHaveBeenCalledTimes(2);
expect(readSnapshot).toHaveBeenCalledTimes(1);
@@ -3398,8 +3355,7 @@ describe("startGatewayConfigReloader", () => {
}),
});
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(targetEnv[envKey]).toBe("c");
harness.emitWrite({
@@ -3573,8 +3529,7 @@ describe("startGatewayConfigReloader", () => {
...makeZeroDebounceHookWrite("same-root-hash"),
afterWrite: { mode: "none", reason: "stale resolved intent" },
});
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
const [, hotConfig] = getOnlyHotReloadCall(harness);
expect(hotConfig).toEqual(freshConfig);
@@ -3630,8 +3585,7 @@ describe("startGatewayConfigReloader", () => {
writtenAtMs: Date.now(),
afterWrite: { mode: "none", reason: "secret-aware writer intent" },
});
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onConfigAccepted).toHaveBeenCalledWith(
runtimeConfig,
@@ -3814,8 +3768,7 @@ describe("startGatewayConfigReloader", () => {
},
});
await vi.runAllTimersAsync();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onConfigAccepted).toHaveBeenCalledTimes(2);
const replayOwnership = harness.onConfigAccepted.mock.calls[1]?.[1];
@@ -3845,8 +3798,7 @@ describe("startGatewayConfigReloader", () => {
await vi.runAllTimersAsync();
const originalOwnership = harness.onConfigAccepted.mock.calls[0]?.[1];
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onConfigAccepted.mock.calls.map((call) => call[3])).toEqual([
{ runtimeApplied: false },
@@ -3979,8 +3931,7 @@ describe("startGatewayConfigReloader", () => {
...makeZeroDebounceHookWrite("same-invalid-root-hash"),
afterWrite: { mode: "restart", reason: "must not replay invalid config" },
});
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onConfigAccepted).not.toHaveBeenCalled();
expect(harness.onRestart).not.toHaveBeenCalled();
@@ -4066,11 +4017,9 @@ describe("startGatewayConfigReloader", () => {
...makeZeroDebounceHookWrite("replay-retry"),
afterWrite: { mode: "restart", reason: "retry original intent" },
});
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(readSnapshot).toHaveBeenCalledTimes(2);
expect(harness.onRestart).toHaveBeenCalledTimes(2);
@@ -4091,8 +4040,7 @@ describe("startGatewayConfigReloader", () => {
});
await vi.runAllTimersAsync();
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(readSnapshot).toHaveBeenCalledOnce();
expect(harness.onRestart).toHaveBeenCalledTimes(2);
@@ -4522,8 +4470,7 @@ describe("startGatewayConfigReloader", () => {
...makeZeroDebounceHookWrite("startup-internal-1"),
afterWrite: { mode: "restart", reason: "live writer owns startup hash" },
});
harness.watcher.emit("change");
await vi.runAllTimersAsync();
await flushWatcherChange(harness);
expect(harness.onRestart).toHaveBeenCalledOnce();
expect(harness.onRestart.mock.calls[0]?.[0].restartReasons).toContain(
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+28 -110
View File
@@ -379,6 +379,17 @@ vi.mock("./server-cron.js", async () => {
};
});
function createRecordedChannelHandlers(events: string[]) {
return {
stop: vi.fn(async (channel: ChannelKind, accountId?: string) => {
events.push(`stop:${channel}:${accountId}`);
}),
start: vi.fn(async (channel: ChannelKind, accountId?: string) => {
events.push(`start:${channel}:${accountId}`);
}),
};
}
function createTestCronReconciliation() {
const complete = vi.fn<() => Promise<void>>(async () => {});
return {
@@ -389,21 +400,11 @@ function createTestCronReconciliation() {
}
function createCronRestartPlan(): GatewayReloadPlan {
return {
return createHotTailPlan({
changedPaths: ["cron"],
restartGateway: false,
restartReasons: [],
hotReasons: ["cron"],
reloadHooks: false,
restartGmailWatcher: false,
restartCron: true,
restartHeartbeat: false,
restartHealthMonitor: false,
reloadPlugins: false,
restartChannels: new Set(),
disposeMcpRuntimes: false,
noopPaths: [],
};
});
}
function createHotTailPlan(overrides: Partial<GatewayReloadPlan> = {}): GatewayReloadPlan {
@@ -1989,21 +1990,12 @@ describe("gateway restart deferral preflight", () => {
...configA,
gateway: { port: 19_001 },
} as OpenClawConfig;
const plan = {
const plan = createHotTailPlan({
changedPaths: ["channels.discord.token", "logging.level"],
restartGateway: false,
restartReasons: [],
hotReasons: ["channels.discord.token"],
reloadHooks: false,
restartGmailWatcher: false,
restartCron: false,
restartHeartbeat: false,
restartHealthMonitor: false,
reloadPlugins: false,
restartChannels: new Set<ChannelKind>(["discord"]),
disposeMcpRuntimes: false,
noopPaths: ["logging.level"],
} satisfies GatewayReloadPlan;
}) satisfies GatewayReloadPlan;
const configRestartPlan = {
...createHotTailPlan(),
changedPaths: ["gateway.port"],
@@ -2717,14 +2709,7 @@ describe("gateway channel hot reload handlers", () => {
it("promotes unlisted accounts to a wholesale restart", async () => {
const events: string[] = [];
const channels = {
stop: vi.fn(async (channel: ChannelKind, accountId?: string) => {
events.push(`stop:${channel}:${accountId}`);
}),
start: vi.fn(async (channel: ChannelKind, accountId?: string) => {
events.push(`start:${channel}:${accountId}`);
}),
};
const channels = createRecordedChannelHandlers(events);
const { applyHotReload } = createReloadHandlersForTest(undefined, channels);
await withChannelReloadsEnabled(async () => {
@@ -2738,14 +2723,7 @@ describe("gateway channel hot reload handlers", () => {
it("promotes unresolvable accounts to a wholesale restart before stopping any account", async () => {
const events: string[] = [];
const channels = {
stop: vi.fn(async (channel: ChannelKind, accountId?: string) => {
events.push(`stop:${channel}:${accountId}`);
}),
start: vi.fn(async (channel: ChannelKind, accountId?: string) => {
events.push(`start:${channel}:${accountId}`);
}),
};
const channels = createRecordedChannelHandlers(events);
const { applyHotReload, logChannels } = createReloadHandlersForTest(undefined, channels);
await withChannelReloadsEnabled(async () => {
@@ -2801,14 +2779,7 @@ describe("gateway channel hot reload handlers", () => {
it("skips per-account restarts for channels already queued for wholesale restart", async () => {
const events: string[] = [];
const channels = {
stop: vi.fn(async (channel: ChannelKind, accountId?: string) => {
events.push(`stop:${channel}:${accountId}`);
}),
start: vi.fn(async (channel: ChannelKind, accountId?: string) => {
events.push(`start:${channel}:${accountId}`);
}),
};
const channels = createRecordedChannelHandlers(events);
const { applyHotReload } = createReloadHandlersForTest(undefined, channels);
await withChannelReloadsEnabled(async () => {
@@ -2866,14 +2837,7 @@ describe("gateway channel hot reload handlers", () => {
it("stops account targets without restarting them while autostart is suppressed", async () => {
const events: string[] = [];
const channels = {
stop: vi.fn(async (channel: ChannelKind, accountId?: string) => {
events.push(`stop:${channel}:${accountId}`);
}),
start: vi.fn(async (channel: ChannelKind, accountId?: string) => {
events.push(`start:${channel}:${accountId}`);
}),
};
const channels = createRecordedChannelHandlers(events);
const { applyHotReload } = createReloadHandlersForTest(
undefined,
channels,
@@ -2899,14 +2863,7 @@ describe("gateway channel hot reload handlers", () => {
it("rechecks agent work admitted after plugin reload leaves the channel running", async () => {
const events: string[] = [];
const channels = {
stop: vi.fn(async (channel: ChannelKind, accountId?: string) => {
events.push(`stop:${channel}:${accountId}`);
}),
start: vi.fn(async (channel: ChannelKind, accountId?: string) => {
events.push(`start:${channel}:${accountId}`);
}),
};
const channels = createRecordedChannelHandlers(events);
const reloadPlugins = vi.fn(async (params): Promise<GatewayPluginReloadResult> => {
await params.beforeReplace(new Set());
hoisted.activeEmbeddedRunCount.value = 1;
@@ -4600,21 +4557,11 @@ describe("gateway plugin hot reload handlers", () => {
try {
await handlers.applyHotReload(
{
createHotTailPlan({
changedPaths: [`env.vars.${envKey}`, "channels.discord.token"],
restartGateway: false,
restartReasons: [],
hotReasons: [`env.vars.${envKey}`, "channels.discord.token"],
reloadHooks: false,
restartGmailWatcher: false,
restartCron: false,
restartHeartbeat: false,
restartHealthMonitor: false,
reloadPlugins: false,
restartChannels: new Set(["discord"]),
disposeMcpRuntimes: false,
noopPaths: [],
},
}),
{},
{
runtimeEnv: runtimeEnv.env,
@@ -4667,21 +4614,11 @@ describe("gateway plugin hot reload handlers", () => {
try {
await handlers.applyHotReload(
{
createHotTailPlan({
changedPaths: [`env.vars.${envKey}`, "channels.discord.token"],
restartGateway: false,
restartReasons: [],
hotReasons: [`env.vars.${envKey}`, "channels.discord.token"],
reloadHooks: false,
restartGmailWatcher: false,
restartCron: false,
restartHeartbeat: false,
restartHealthMonitor: false,
reloadPlugins: false,
restartChannels: new Set(["discord"]),
disposeMcpRuntimes: false,
noopPaths: [],
},
}),
nextConfig,
{
runtimeEnv: runtimeEnv.env,
@@ -4833,21 +4770,12 @@ describe("gateway plugin hot reload handlers", () => {
vi.useFakeTimers();
const reload = handlers.applyHotReload(
{
createHotTailPlan({
changedPaths: ["hooks.path", "plugins.enabled"],
restartGateway: false,
restartReasons: [],
hotReasons: ["hooks.path", "plugins.enabled"],
reloadHooks: true,
restartGmailWatcher: false,
restartCron: false,
restartHeartbeat: false,
restartHealthMonitor: false,
reloadPlugins: true,
restartChannels: new Set(),
disposeMcpRuntimes: false,
noopPaths: [],
},
}),
{ hooks: { enabled: true, token: "token", path: "/next" } },
{
isCurrent: () => true,
@@ -5366,21 +5294,11 @@ describe("gateway plugin hot reload handlers", () => {
});
describe("deferred channel reload abort generation", () => {
const abortChannelReloadPlan: GatewayReloadPlan = {
const abortChannelReloadPlan: GatewayReloadPlan = createHotTailPlan({
changedPaths: ["channels.whatsapp.enabled"],
restartGateway: false,
restartReasons: [],
hotReasons: ["channels"],
reloadHooks: false,
restartGmailWatcher: false,
restartCron: false,
restartHeartbeat: false,
restartHealthMonitor: false,
reloadPlugins: false,
restartChannels: new Set(["whatsapp"]),
disposeMcpRuntimes: false,
noopPaths: [],
};
});
afterEach(() => {
hoisted.activeTaskCount.value = 0;