mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 00:18:26 +00:00
refactor(gateway): dedupe auth and discord monitor suites
This commit is contained in:
@@ -8,31 +8,27 @@ describe("resolveDiscordDmCommandAccess", () => {
|
||||
tag: "alice#0001",
|
||||
};
|
||||
|
||||
it("allows open DMs and keeps command auth enabled without allowlist entries", async () => {
|
||||
const result = await resolveDiscordDmCommandAccess({
|
||||
async function resolveOpenDmAccess(configuredAllowFrom: string[]) {
|
||||
return await resolveDiscordDmCommandAccess({
|
||||
accountId: "default",
|
||||
dmPolicy: "open",
|
||||
configuredAllowFrom: [],
|
||||
configuredAllowFrom,
|
||||
sender,
|
||||
allowNameMatching: false,
|
||||
useAccessGroups: true,
|
||||
readStoreAllowFrom: async () => [],
|
||||
});
|
||||
}
|
||||
|
||||
it("allows open DMs and keeps command auth enabled without allowlist entries", async () => {
|
||||
const result = await resolveOpenDmAccess([]);
|
||||
|
||||
expect(result.decision).toBe("allow");
|
||||
expect(result.commandAuthorized).toBe(true);
|
||||
});
|
||||
|
||||
it("marks command auth true when sender is allowlisted", async () => {
|
||||
const result = await resolveDiscordDmCommandAccess({
|
||||
accountId: "default",
|
||||
dmPolicy: "open",
|
||||
configuredAllowFrom: ["discord:123"],
|
||||
sender,
|
||||
allowNameMatching: false,
|
||||
useAccessGroups: true,
|
||||
readStoreAllowFrom: async () => [],
|
||||
});
|
||||
const result = await resolveOpenDmAccess(["discord:123"]);
|
||||
|
||||
expect(result.decision).toBe("allow");
|
||||
expect(result.commandAuthorized).toBe(true);
|
||||
|
||||
@@ -168,6 +168,18 @@ function getLastDispatchCtx():
|
||||
return params?.ctx;
|
||||
}
|
||||
|
||||
async function runProcessDiscordMessage(ctx: unknown): Promise<void> {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
await processDiscordMessage(ctx as any);
|
||||
}
|
||||
|
||||
async function runInPartialStreamMode(): Promise<void> {
|
||||
const ctx = await createBaseContext({
|
||||
discordConfig: { streamMode: "partial" },
|
||||
});
|
||||
await runProcessDiscordMessage(ctx);
|
||||
}
|
||||
|
||||
describe("processDiscordMessage ack reactions", () => {
|
||||
it("skips ack reactions for group-mentions when mentions are not required", async () => {
|
||||
const ctx = await createBaseContext({
|
||||
@@ -543,12 +555,7 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 0 } };
|
||||
});
|
||||
|
||||
const ctx = await createBaseContext({
|
||||
discordConfig: { streamMode: "partial" },
|
||||
});
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
await processDiscordMessage(ctx as any);
|
||||
await runInPartialStreamMode();
|
||||
|
||||
const updates = draftStream.update.mock.calls.map((call) => call[0]);
|
||||
for (const text of updates) {
|
||||
@@ -567,12 +574,7 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 0 } };
|
||||
});
|
||||
|
||||
const ctx = await createBaseContext({
|
||||
discordConfig: { streamMode: "partial" },
|
||||
});
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
await processDiscordMessage(ctx as any);
|
||||
await runInPartialStreamMode();
|
||||
|
||||
expect(draftStream.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -167,6 +167,24 @@ async function runSubmitButton(params: {
|
||||
return submitInteraction;
|
||||
}
|
||||
|
||||
async function runModelSelect(params: {
|
||||
context: ModelPickerContext;
|
||||
data?: PickerSelectData;
|
||||
userId?: string;
|
||||
values?: string[];
|
||||
}) {
|
||||
const select = createDiscordModelPickerFallbackSelect(params.context);
|
||||
const selectInteraction = createInteraction({
|
||||
userId: params.userId ?? "owner",
|
||||
values: params.values ?? ["gpt-4o"],
|
||||
});
|
||||
await select.run(
|
||||
selectInteraction as unknown as PickerSelectInteraction,
|
||||
params.data ?? createModelsViewSelectData(),
|
||||
);
|
||||
return selectInteraction;
|
||||
}
|
||||
|
||||
function expectDispatchedModelSelection(params: {
|
||||
dispatchSpy: { mock: { calls: Array<[unknown]> } };
|
||||
model: string;
|
||||
@@ -270,15 +288,7 @@ describe("Discord model picker interactions", () => {
|
||||
.spyOn(dispatcherModule, "dispatchReplyWithDispatcher")
|
||||
.mockResolvedValue({} as never);
|
||||
|
||||
const select = createDiscordModelPickerFallbackSelect(context);
|
||||
const selectInteraction = createInteraction({
|
||||
userId: "owner",
|
||||
values: ["gpt-4o"],
|
||||
});
|
||||
|
||||
const selectData = createModelsViewSelectData();
|
||||
|
||||
await select.run(selectInteraction as unknown as PickerSelectInteraction, selectData);
|
||||
const selectInteraction = await runModelSelect({ context });
|
||||
|
||||
expect(selectInteraction.update).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchSpy).not.toHaveBeenCalled();
|
||||
@@ -315,15 +325,7 @@ describe("Discord model picker interactions", () => {
|
||||
.spyOn(timeoutModule, "withTimeout")
|
||||
.mockRejectedValue(new Error("timeout"));
|
||||
|
||||
const select = createDiscordModelPickerFallbackSelect(context);
|
||||
const selectInteraction = createInteraction({
|
||||
userId: "owner",
|
||||
values: ["gpt-4o"],
|
||||
});
|
||||
|
||||
const selectData = createModelsViewSelectData();
|
||||
|
||||
await select.run(selectInteraction as unknown as PickerSelectInteraction, selectData);
|
||||
await runModelSelect({ context });
|
||||
|
||||
const button = createDiscordModelPickerFallbackButton(context);
|
||||
const submitInteraction = createInteraction({ userId: "owner" });
|
||||
|
||||
@@ -143,6 +143,11 @@ describe("runDiscordGatewayLifecycle", () => {
|
||||
return { emitter, gateway };
|
||||
}
|
||||
|
||||
async function emitGatewayOpenAndWait(emitter: EventEmitter, delayMs = 30000): Promise<void> {
|
||||
emitter.emit("debug", "WebSocket connection opened");
|
||||
await vi.advanceTimersByTimeAsync(delayMs);
|
||||
}
|
||||
|
||||
it("cleans up thread bindings when exec approvals startup fails", async () => {
|
||||
const { runDiscordGatewayLifecycle } = await import("./provider.lifecycle.js");
|
||||
const { lifecycleParams, start, stop, threadStop, releaseEarlyGatewayErrorGuard } =
|
||||
@@ -260,12 +265,9 @@ describe("runDiscordGatewayLifecycle", () => {
|
||||
});
|
||||
getDiscordGatewayEmitterMock.mockReturnValueOnce(emitter);
|
||||
waitForDiscordGatewayStopMock.mockImplementationOnce(async () => {
|
||||
emitter.emit("debug", "WebSocket connection opened");
|
||||
await vi.advanceTimersByTimeAsync(30000);
|
||||
emitter.emit("debug", "WebSocket connection opened");
|
||||
await vi.advanceTimersByTimeAsync(30000);
|
||||
emitter.emit("debug", "WebSocket connection opened");
|
||||
await vi.advanceTimersByTimeAsync(30000);
|
||||
await emitGatewayOpenAndWait(emitter);
|
||||
await emitGatewayOpenAndWait(emitter);
|
||||
await emitGatewayOpenAndWait(emitter);
|
||||
});
|
||||
|
||||
const { lifecycleParams } = createLifecycleHarness({ gateway });
|
||||
@@ -299,22 +301,17 @@ describe("runDiscordGatewayLifecycle", () => {
|
||||
});
|
||||
getDiscordGatewayEmitterMock.mockReturnValueOnce(emitter);
|
||||
waitForDiscordGatewayStopMock.mockImplementationOnce(async () => {
|
||||
emitter.emit("debug", "WebSocket connection opened");
|
||||
await vi.advanceTimersByTimeAsync(30000);
|
||||
await emitGatewayOpenAndWait(emitter);
|
||||
|
||||
// Successful reconnect (READY/RESUMED sets isConnected=true), then
|
||||
// quick drop before the HELLO timeout window finishes.
|
||||
gateway.isConnected = true;
|
||||
emitter.emit("debug", "WebSocket connection opened");
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
await emitGatewayOpenAndWait(emitter, 10);
|
||||
emitter.emit("debug", "WebSocket connection closed with code 1006");
|
||||
gateway.isConnected = false;
|
||||
|
||||
emitter.emit("debug", "WebSocket connection opened");
|
||||
await vi.advanceTimersByTimeAsync(30000);
|
||||
|
||||
emitter.emit("debug", "WebSocket connection opened");
|
||||
await vi.advanceTimersByTimeAsync(30000);
|
||||
await emitGatewayOpenAndWait(emitter);
|
||||
await emitGatewayOpenAndWait(emitter);
|
||||
});
|
||||
|
||||
const { lifecycleParams } = createLifecycleHarness({ gateway });
|
||||
|
||||
@@ -38,24 +38,32 @@ async function fetchDiscordApplicationMe(
|
||||
timeoutMs: number,
|
||||
fetcher: typeof fetch,
|
||||
): Promise<{ id?: string; flags?: number } | undefined> {
|
||||
try {
|
||||
const appResponse = await fetchDiscordApplicationMeResponse(token, timeoutMs, fetcher);
|
||||
if (!appResponse || !appResponse.ok) {
|
||||
return undefined;
|
||||
}
|
||||
return (await appResponse.json()) as { id?: string; flags?: number };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDiscordApplicationMeResponse(
|
||||
token: string,
|
||||
timeoutMs: number,
|
||||
fetcher: typeof fetch,
|
||||
): Promise<Response | undefined> {
|
||||
const normalized = normalizeDiscordToken(token);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const res = await fetchWithTimeout(
|
||||
`${DISCORD_API_BASE}/oauth2/applications/@me`,
|
||||
{ headers: { Authorization: `Bot ${normalized}` } },
|
||||
timeoutMs,
|
||||
getResolvedFetch(fetcher),
|
||||
);
|
||||
if (!res.ok) {
|
||||
return undefined;
|
||||
}
|
||||
return (await res.json()) as { id?: string; flags?: number };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return await fetchWithTimeout(
|
||||
`${DISCORD_API_BASE}/oauth2/applications/@me`,
|
||||
{ headers: { Authorization: `Bot ${normalized}` } },
|
||||
timeoutMs,
|
||||
getResolvedFetch(fetcher),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveDiscordPrivilegedIntentsFromFlags(
|
||||
@@ -198,17 +206,14 @@ export async function fetchDiscordApplicationId(
|
||||
timeoutMs: number,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<string | undefined> {
|
||||
const normalized = normalizeDiscordToken(token);
|
||||
if (!normalized) {
|
||||
if (!normalizeDiscordToken(token)) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const res = await fetchWithTimeout(
|
||||
`${DISCORD_API_BASE}/oauth2/applications/@me`,
|
||||
{ headers: { Authorization: `Bot ${normalized}` } },
|
||||
timeoutMs,
|
||||
getResolvedFetch(fetcher),
|
||||
);
|
||||
const res = await fetchDiscordApplicationMeResponse(token, timeoutMs, fetcher);
|
||||
if (!res) {
|
||||
return undefined;
|
||||
}
|
||||
if (res.ok) {
|
||||
const json = (await res.json()) as { id?: string };
|
||||
if (json?.id) {
|
||||
|
||||
Reference in New Issue
Block a user