





















@@ -28,6 +28,8 @@ const mocks = vi.hoisted(() => ({
2828getChannelPlugin: vi.fn(),
2929loadOpenClawPlugins: vi.fn(),
3030applyPluginAutoEnable: vi.fn(),
31+getRuntimeConfigSnapshot: vi.fn(),
32+getRuntimeConfigSourceSnapshot: vi.fn(),
3133}));
32343335vi.mock("../../config/config.js", async () => {
@@ -79,6 +81,17 @@ vi.mock("../../config/plugin-auto-enable.js", () => ({
7981mocks.applyPluginAutoEnable({ config, env }),
8082}));
818384+vi.mock("../../config/runtime-snapshot.js", async () => {
85+const actual = await vi.importActual<typeof import("../../config/runtime-snapshot.js")>(
86+"../../config/runtime-snapshot.js",
87+);
88+return {
89+ ...actual,
90+getRuntimeConfigSnapshot: mocks.getRuntimeConfigSnapshot,
91+getRuntimeConfigSourceSnapshot: mocks.getRuntimeConfigSourceSnapshot,
92+};
93+});
94+8295vi.mock("../../plugins/loader.js", () => ({
8396loadOpenClawPlugins: mocks.loadOpenClawPlugins,
8497resolveRuntimePluginRegistry: vi.fn(),
@@ -280,6 +293,8 @@ describe("gateway send mirroring", () => {
280293changes: [],
281294autoEnabledReasons: {},
282295}));
296+mocks.getRuntimeConfigSnapshot.mockReturnValue(null);
297+mocks.getRuntimeConfigSourceSnapshot.mockReturnValue(null);
283298mocks.resolveOutboundTarget.mockReturnValue({ ok: true, to: "resolved" });
284299mocks.resolveOutboundSessionRoute.mockImplementation(
285300async ({ agentId, channel }: { agentId?: string; channel?: string }) => ({
@@ -303,6 +318,251 @@ describe("gateway send mirroring", () => {
303318});
304319});
305320321+it("uses the resolved runtime config for message.action when the source snapshot matches", async () => {
322+const sourceConfig = {
323+channels: {
324+discord: {
325+accounts: {
326+drclaw: {
327+token: {
328+source: "env",
329+provider: "default",
330+id: "DISCORD_BOT_TOKEN_DRCLAW",
331+},
332+},
333+},
334+},
335+},
336+};
337+const runtimeConfig = {
338+channels: {
339+discord: {
340+accounts: {
341+drclaw: {
342+token: "resolved-token",
343+},
344+},
345+},
346+},
347+};
348+mocks.applyPluginAutoEnable.mockImplementation(({ config }) => ({
349+ config,
350+changes: [],
351+autoEnabledReasons: {},
352+}));
353+mocks.getRuntimeConfigSnapshot.mockReturnValue(runtimeConfig);
354+mocks.getRuntimeConfigSourceSnapshot.mockReturnValue(sourceConfig);
355+356+const context = {
357+ ...makeContext(),
358+getRuntimeConfig: () => sourceConfig,
359+} as unknown as GatewayRequestContext;
360+const respond = vi.fn();
361+await sendHandlers["message.action"]({
362+params: {
363+channel: "discord",
364+action: "channel-info",
365+params: { channelId: "123", accountId: "drclaw" },
366+idempotencyKey: "idem-action-runtime-config",
367+} as never,
368+ respond,
369+ context,
370+req: { type: "req", id: "1", method: "message.action" },
371+client: null as never,
372+isWebchatConnect: () => false,
373+});
374+375+expect(mocks.getRuntimeConfigSnapshot).toHaveBeenCalledTimes(1);
376+expect(mocks.getRuntimeConfigSourceSnapshot).toHaveBeenCalledTimes(1);
377+expect(lastDispatchChannelMessageActionCall()?.cfg).toBe(runtimeConfig);
378+const response = firstRespondCall(respond);
379+expect(response?.[0]).toBe(true);
380+});
381+382+it("matches message.action runtime config against the canonical pre-auto-enable source config", async () => {
383+const sourceConfig = {
384+channels: {
385+discord: {
386+accounts: {
387+drclaw: {
388+token: {
389+source: "env",
390+provider: "default",
391+id: "DISCORD_BOT_TOKEN_DRCLAW",
392+},
393+},
394+},
395+},
396+},
397+};
398+const autoEnabledSourceConfig = {
399+channels: {
400+discord: {
401+enabled: true,
402+accounts: {
403+drclaw: {
404+token: {
405+source: "env",
406+provider: "default",
407+id: "DISCORD_BOT_TOKEN_DRCLAW",
408+},
409+},
410+},
411+},
412+},
413+plugins: { allow: ["discord"] },
414+};
415+const autoEnabledRuntimeConfig = {
416+channels: {
417+discord: {
418+enabled: true,
419+accounts: {
420+drclaw: {
421+token: "resolved-token",
422+},
423+},
424+},
425+},
426+plugins: { allow: ["discord"] },
427+};
428+mocks.applyPluginAutoEnable
429+.mockReturnValueOnce({
430+config: autoEnabledSourceConfig,
431+changes: [{ path: "channels.discord.enabled", value: true }],
432+autoEnabledReasons: {},
433+})
434+.mockReturnValueOnce({
435+config: autoEnabledRuntimeConfig,
436+changes: [{ path: "channels.discord.enabled", value: true }],
437+autoEnabledReasons: {},
438+});
439+mocks.getRuntimeConfigSnapshot.mockReturnValue(autoEnabledRuntimeConfig);
440+mocks.getRuntimeConfigSourceSnapshot.mockReturnValue(sourceConfig);
441+442+const context = {
443+ ...makeContext(),
444+getRuntimeConfig: () => sourceConfig,
445+} as unknown as GatewayRequestContext;
446+const respond = vi.fn();
447+await sendHandlers["message.action"]({
448+params: {
449+channel: "discord",
450+action: "channel-info",
451+params: { channelId: "123", accountId: "drclaw" },
452+idempotencyKey: "idem-action-runtime-config-auto-enabled",
453+} as never,
454+ respond,
455+ context,
456+req: { type: "req", id: "1", method: "message.action" },
457+client: null as never,
458+isWebchatConnect: () => false,
459+});
460+461+expect(lastDispatchChannelMessageActionCall()?.cfg).toBe(autoEnabledRuntimeConfig);
462+expect(mocks.applyPluginAutoEnable).toHaveBeenNthCalledWith(1, {
463+config: sourceConfig,
464+env: undefined,
465+});
466+expect(mocks.applyPluginAutoEnable).toHaveBeenNthCalledWith(2, {
467+config: autoEnabledRuntimeConfig,
468+env: undefined,
469+});
470+const response = firstRespondCall(respond);
471+expect(response?.[0]).toBe(true);
472+});
473+474+it("keeps the post-auto-enable request config for message.action when the runtime source snapshot does not match", async () => {
475+const sourceConfig = {
476+channels: {
477+discord: {
478+accounts: {
479+drclaw: {
480+token: {
481+source: "env",
482+provider: "default",
483+id: "DISCORD_BOT_TOKEN_DRCLAW",
484+},
485+},
486+},
487+},
488+},
489+};
490+const autoEnabledRequestConfig = {
491+channels: {
492+discord: {
493+enabled: true,
494+accounts: {
495+drclaw: {
496+token: {
497+source: "env",
498+provider: "default",
499+id: "DISCORD_BOT_TOKEN_DRCLAW",
500+},
501+},
502+},
503+},
504+},
505+plugins: { allow: ["discord"] },
506+};
507+mocks.applyPluginAutoEnable.mockReturnValue({
508+config: autoEnabledRequestConfig,
509+changes: [{ path: "channels.discord.enabled", value: true }],
510+autoEnabledReasons: {},
511+});
512+mocks.getRuntimeConfigSnapshot.mockReturnValue({
513+channels: {
514+discord: {
515+accounts: {
516+drclaw: { token: "stale-runtime-token" },
517+},
518+},
519+},
520+});
521+mocks.getRuntimeConfigSourceSnapshot.mockReturnValue({
522+channels: {
523+discord: {
524+accounts: {
525+other: { token: "different-source" },
526+},
527+},
528+},
529+});
530+531+const context = {
532+ ...makeContext(),
533+getRuntimeConfig: () => sourceConfig,
534+} as unknown as GatewayRequestContext;
535+await sendHandlers["message.action"]({
536+params: {
537+channel: "discord",
538+action: "channel-info",
539+params: { channelId: "123", accountId: "drclaw" },
540+idempotencyKey: "idem-action-stale-runtime-config",
541+} as never,
542+respond: vi.fn(),
543+ context,
544+req: { type: "req", id: "1", method: "message.action" },
545+client: null as never,
546+isWebchatConnect: () => false,
547+});
548+549+expect(lastDispatchChannelMessageActionCall()?.cfg).toBe(autoEnabledRequestConfig);
550+});
551+552+it("does not read the runtime config snapshot for send requests", async () => {
553+mockDeliverySuccess("m-no-runtime-config-read");
554+555+await runSend({
556+to: "channel:C1",
557+message: "hi",
558+channel: "slack",
559+idempotencyKey: "idem-send-no-runtime-config-read",
560+});
561+562+expect(mocks.getRuntimeConfigSnapshot).not.toHaveBeenCalled();
563+expect(mocks.getRuntimeConfigSourceSnapshot).not.toHaveBeenCalled();
564+});
565+306566it("dedupes concurrent message.action requests while inflight", async () => {
307567const context = makeContext();
308568const firstRespond = vi.fn();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。