refactor(gateway): dedupe auth and discord monitor suites

This commit is contained in:
Peter Steinberger
2026-03-02 21:30:43 +00:00
parent ab8b8dae70
commit 5f0cbd0edc
14 changed files with 434 additions and 500 deletions

View File

@@ -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);

View File

@@ -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();
});

View File

@@ -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" });

View File

@@ -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 });

View File

@@ -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) {