Files
openclaw/extensions/discord/src/monitor/provider.deploy.test.ts
T
Peter Steinberger 36e0ac3576 fix(logs): clean up gateway and channel startup/shutdown log output (#104174)
* fix(logs): clean up gateway and channel startup/shutdown log output

Scope Discord slash-command deploy REST diagnostics to command routes so
concurrent startup traffic (voice-state probes, channel lookups) keeps its
owner's error handling; make per-request deploy error lines verbose-only and
drop JSON bodies that only repeat message+code. Log allowlist summaries one
line per call so unresolved lines keep their timestamp/subsystem prefix, and
skip identity lookups that resolved to themselves. Remove embedded subsystem
prefixes, demote routine signal/shutdown/force/postbuild/diag chatter to
debug or verbose, merge the duplicate Control UI build notices, drop doctor's
duplicate backup line, and name the config surface or platform limit in the
transcripts autoStart and command-limit warnings.

Fixes #104163

* fix(slack): keep bare-name allowlist resolutions in startup log summary

Only omit identity lookups where the input already is the resolved id;
name-based lookups that translated to an id stay logged even when the
display name matches the input.

* fix(telegram): keep isolated-ingress readiness marker on the runtime log

test/e2e/qa-lab telegram-bot-token-runtime waits for this line via the
injected RuntimeEnv.log; verbose-only logging would deterministically time
out that live proof. Comment the contract at the call site.
2026-07-10 22:13:33 -07:00

137 lines
4.8 KiB
TypeScript

// Discord tests cover slash-command deploy REST logging behavior.
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { describe, expect, it, type Mock, vi } from "vitest";
import type { Client } from "../internal/discord.js";
import { formatDiscordDeployErrorDetails } from "./provider.deploy-errors.js";
import { runDiscordCommandDeployInBackground } from "./provider.deploy.js";
type RestFn = (path: string, data?: unknown, query?: unknown) => Promise<unknown>;
type RestMock = {
get: Mock<RestFn>;
post: Mock<RestFn>;
put: Mock<RestFn>;
patch: Mock<RestFn>;
delete: Mock<RestFn>;
};
function createRestMock(): RestMock {
return {
get: vi.fn<RestFn>(async () => undefined),
post: vi.fn<RestFn>(async () => undefined),
put: vi.fn<RestFn>(async () => undefined),
patch: vi.fn<RestFn>(async () => undefined),
delete: vi.fn<RestFn>(async () => undefined),
};
}
function runDeploy(params: {
rest: RestMock;
deployCommands: (rest: RestMock) => Promise<void>;
shouldLogVerbose?: boolean;
}) {
const log = vi.fn<(message: string) => void>();
const error = vi.fn<(message: string) => void>();
const client = {
rest: params.rest,
deployCommands: vi.fn(async () => params.deployCommands(params.rest)),
};
runDiscordCommandDeployInBackground({
client: client as unknown as Client,
runtime: { log, error } as unknown as RuntimeEnv,
enabled: true,
accountId: "default",
startupStartedAt: Date.now(),
shouldLogVerbose: () => params.shouldLogVerbose ?? false,
isVerbose: () => false,
});
return { log, error, client };
}
function joinedCalls(mock: Mock<(message: string) => void>): string {
return mock.mock.calls.map((call) => call[0]).join("\n");
}
describe("discord slash-command deploy REST logging", () => {
it("passes non-command REST traffic through without deploy labels", async () => {
const rest = createRestMock();
rest.get.mockImplementation(async (path: string) => {
if (path.includes("/voice-states/")) {
throw Object.assign(new Error("Unknown Voice State"), { status: 404 });
}
return undefined;
});
let deployFinished = false;
const { log, error } = runDeploy({
rest,
shouldLogVerbose: true,
deployCommands: async (wrapped) => {
await wrapped.get("/applications/app-1/commands");
// Concurrent non-deploy traffic (voice-state probe) during the deploy
// window must keep its caller-owned error handling.
await expect(wrapped.get("/guilds/1/voice-states/2")).rejects.toThrow(
"Unknown Voice State",
);
deployFinished = true;
},
});
await vi.waitFor(() => expect(deployFinished).toBe(true));
const logged = joinedCalls(log);
expect(logged).toContain("native-slash-command-deploy-rest:get:start");
expect(logged).toContain("path=/applications/app-1/commands");
expect(logged).not.toContain("voice-states");
expect(logged).not.toContain("slash command deploy failed");
expect(joinedCalls(error)).not.toContain("voice-states");
});
it("logs one warning per deploy failure at default verbosity", async () => {
const rest = createRestMock();
const deployError = Object.assign(new Error("Missing Access"), {
status: 403,
discordCode: 50001,
rawBody: { message: "Missing Access", code: 50001 },
});
rest.post.mockRejectedValue(deployError);
const { log, error, client } = runDeploy({
rest,
shouldLogVerbose: false,
deployCommands: async (wrapped) => {
await wrapped.post("/applications/app-1/commands", {
body: [{ name: "skill" }],
} as never);
},
});
await vi.waitFor(() => expect(client.deployCommands).toHaveBeenCalledTimes(1));
await vi.waitFor(() => expect(log).toHaveBeenCalled());
const warnings = log.mock.calls
.map((call) => call[0])
.filter((message) => message.includes("slash command deploy failed"));
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain("(message send/receive unaffected)");
// Per-request rest:error lines are verbose-only diagnostics.
expect(error).not.toHaveBeenCalled();
});
});
describe("formatDiscordDeployErrorDetails", () => {
it("omits bodies that only repeat the message and code", () => {
expect(
formatDiscordDeployErrorDetails({
status: 400,
discordCode: 30032,
rawBody: { message: "Maximum number of application commands reached (100).", code: 30032 },
}),
).toBe(" (status=400, code=30032)");
});
it("keeps bodies that carry extra fields", () => {
const details = formatDiscordDeployErrorDetails({
status: 400,
discordCode: 50035,
rawBody: { message: "Invalid Form Body", code: 50035, errors: { "0": { name: {} } } },
});
expect(details).toContain("body=");
});
});