Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions test/gateway-send.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
import { once } from "node:events";
import { createServer } from "node:http";

import { GATEWAY_MAX_RESPONSE_BYTES, getTranscriptTail, sendPrompt } from "../src/core/gateway.js";

Expand All @@ -13,13 +15,39 @@ function mockGateway(t, send) {
});
}

test("sendPrompt refuses a live gateway host in test mode before any request", async (t) => {
const fetchMock = t.mock.method(globalThis, "fetch", async () => new Response("{}", { status: 200 }));
test("test mode: a loopback gateway is served, a live gateway host is refused before any request", async (t) => {
const received = [];
const server = createServer(async (req, res) => {
let text = "";
for await (const chunk of req) text += chunk;
received.push({ url: req.url, body: JSON.parse(text) });
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(req.url === "/api/listAgents" ? roster : { messageId: "m-loop" }));
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
t.after(() => new Promise((resolve) => server.close(resolve)));
const local = { gatewayUrl: "http://127.0.0.1:" + server.address().port, gatewayToken: "t" };

const out = await sendPrompt(local, "General", "hi");
assert.deepEqual(out, {
target: { id: "bot-1", name: "General", title: "", description: "", avatarShape: "", avatarColor: "", isGroup: false, memberIds: [] },
result: { messageId: "m-loop" },
delivery: "accepted",
messageId: "m-loop",
});
assert.deepEqual(received.map((r) => r.url), ["/api/listAgents", "/api/sendPrompt"]);
assert.equal(received[1].body.prompt, "hi");

// A non-routable host, and the production escape hatch switched on: production
// policy would let this through, test mode must still refuse it.
process.env.GROK_BOT_ALLOW_ANY_GATEWAY = "1";
t.after(() => { delete process.env.GROK_BOT_ALLOW_ANY_GATEWAY; });
await assert.rejects(
sendPrompt({ gatewayUrl: "https://box.cursor.sh", gatewayToken: "t" }, "General", "hi"),
/test mode/i,
sendPrompt({ gatewayUrl: "https://gateway.invalid", gatewayToken: "t" }, "General", "hi"),
{ message: 'Rejected gateway URL host "gateway.invalid": test mode (GROK_BOT_TEST / NODE_ENV=test) only allows http(s) loopback gateways.' },
);
assert.equal(fetchMock.mock.callCount(), 0);
assert.equal(received.length, 2, "the refused send reached no server");
});

test("sendPrompt accepts only a confirmed messageId receipt", async (t) => {
Expand Down
12 changes: 8 additions & 4 deletions test/url-policy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,14 @@ test("test mode allows only loopback, for gateway and backend, and ignores escap
withEnv({ GROK_BOT_ALLOW_ANY_GATEWAY: "1", GROK_BOT_ALLOW_LOCAL_GATEWAY: null, ...env }, () => {
assert.equal(assertAllowedCredentialUrl("http://127.0.0.1:1340/"), "http://127.0.0.1:1340");
assert.equal(assertAllowedCredentialUrl("http://localhost:1340", { kind: "backend" }), "http://localhost:1340");
assert.throws(() => assertAllowedCredentialUrl("https://box.cursor.sh"), /test mode/i);
assert.throws(() => assertAllowedCredentialUrl("https://api2.cursor.sh", { kind: "backend" }), /test mode/i);
assert.throws(() => assertAllowedCredentialUrl("https://evil.example"), /test mode/i);
assert.throws(() => assertAllowedCredentialUrl("ws://127.0.0.1:1340"), /test mode/i);
assert.throws(() => assertAllowedCredentialUrl("https://box.cursor.sh"), {
message: 'Rejected gateway URL host "box.cursor.sh": test mode (GROK_BOT_TEST / NODE_ENV=test) only allows http(s) loopback gateways.',
});
assert.throws(() => assertAllowedCredentialUrl("https://api2.cursor.sh", { kind: "backend" }), {
message: 'Rejected backend URL host "api2.cursor.sh": test mode (GROK_BOT_TEST / NODE_ENV=test) only allows http(s) loopback gateways.',
});
assert.throws(() => assertAllowedCredentialUrl("https://evil.example"), /^Error: Rejected gateway URL host "evil.example": test mode/);
assert.throws(() => assertAllowedCredentialUrl("ws://127.0.0.1:1340"), /^Error: Rejected gateway URL host "127.0.0.1": test mode/);
});
}
});
Expand Down
37 changes: 35 additions & 2 deletions tests/route-unit/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,6 @@ beforeAll(async () => {
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
const { port } = server.address() as AddressInfo;
for (const key of envKeys) savedEnv[key] = process.env[key];
// rstest.route-unit.config.ts sets this; url-policy then refuses every non-loopback gateway.
expect(process.env.GROK_BOT_TEST).toBe('1');
process.env.GROK_BOT_GATEWAY_URL = `http://127.0.0.1:${port}`;
process.env.GROK_BOT_GATEWAY_TOKEN = 'test-token';
process.env.GROK_BOT_ALLOW_LOCAL_GATEWAY = '1';
Expand Down Expand Up @@ -398,6 +396,41 @@ describe('grok-bot MCP server', () => {
expect(text).not.toContain('test-token');
});

it('refuses a live gateway host under the test runner and still serves the loopback fake', async () => {
// rstest.route-unit.config.ts sets GROK_BOT_TEST=1; the URL policy then turns any
// non-loopback gateway into a tool error before a request is built.
// Non-routable host plus the production escape hatch: production policy would
// proceed, test mode must refuse, and a regression cannot reach a real gateway.
const loopbackUrl = process.env.GROK_BOT_GATEWAY_URL;
process.env.GROK_BOT_GATEWAY_URL = 'https://gateway.invalid';
process.env.GROK_BOT_ALLOW_ANY_GATEWAY = '1';
try {
const live = await invokeMcpTool('gbot_send', {
input: { message: 'must not leave the machine', target: 'General' },
server: 'grok-bot',
});
expect(live.isError).toBe(true);
expect(contentText(live.content)).toBe(
'Rejected gateway URL host "gateway.invalid": test mode (GROK_BOT_TEST / NODE_ENV=test) only allows http(s) loopback gateways.',
);
expect(calls).toEqual([]);
} finally {
process.env.GROK_BOT_GATEWAY_URL = loopbackUrl;
delete process.env.GROK_BOT_ALLOW_ANY_GATEWAY;
}
const local = await invokeMcpTool('gbot_send', {
input: { message: 'loopback is fine', target: 'General' },
server: 'grok-bot',
});
expect(local.structuredContent).toEqual({
delivery: 'accepted',
messageId: 'm-1',
result: { messageId: 'm-1' },
target: { id: 'bot-1', kind: 'bot', name: 'General' },
});
expect(calls[1]).toMatchObject({ body: { agentId: 'bot-1', prompt: 'loopback is fine' }, method: 'sendPrompt' });
});

it('surfaces an unknown target as a tool error without sending anything', async () => {
const result = await invokeMcpTool('gbot_send', {
input: { message: 'x', target: 'Nobody' },
Expand Down