Compare commits
2
Commits
35e29316df
...
ea8f4c3df3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea8f4c3df3 | ||
|
|
6d2eb6e22a |
@@ -1,11 +1,18 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { serviceRoutes } from "../src/routes/services.js";
|
import { serviceRoutes } from "../src/routes/services.js";
|
||||||
|
import { config } from "../src/config.js";
|
||||||
|
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
const calls: Array<{ url: string; body: Record<string, unknown> }> = [];
|
const calls: Array<{ url: string; body: Record<string, unknown>; headers: Headers }> = [];
|
||||||
globalThis.fetch = (async (input, init) => {
|
globalThis.fetch = (async (input, init) => {
|
||||||
const body = JSON.parse(String(init?.body ?? "{}")) as Record<string, unknown>;
|
const body = JSON.parse(String(init?.body ?? "{}")) as Record<string, unknown>;
|
||||||
calls.push({ url: String(input), body });
|
calls.push({ url: String(input), body, headers: new Headers(init?.headers) });
|
||||||
|
if (body.action === "force_forbidden") {
|
||||||
|
return new Response(JSON.stringify({ detail: "Invalid A2A auth token" }), { status: 403, headers: { "content-type": "application/json" } });
|
||||||
|
}
|
||||||
|
if (body.action === "agent_error") {
|
||||||
|
return new Response(JSON.stringify({ task_id: "a2a-task", status: "completed", messages: [{ type: "agent_error", action: "search_failed", data: { code: "job_boards_unavailable" } }] }), { status: 200, headers: { "content-type": "application/json" } });
|
||||||
|
}
|
||||||
return new Response(JSON.stringify({ task_id: "a2a-task", status: "completed", messages: [] }), { status: 200, headers: { "content-type": "application/json" } });
|
return new Response(JSON.stringify({ task_id: "a2a-task", status: "completed", messages: [] }), { status: 200, headers: { "content-type": "application/json" } });
|
||||||
}) as typeof fetch;
|
}) as typeof fetch;
|
||||||
const app = serviceRoutes({ skipAuth: true });
|
const app = serviceRoutes({ skipAuth: true });
|
||||||
@@ -24,6 +31,15 @@ try {
|
|||||||
const matchmakingResponse = await app.request("http://backend.test/matchmaking/a2a", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "mark_applied", params: { opportunity_id: "job-1" } }) });
|
const matchmakingResponse = await app.request("http://backend.test/matchmaking/a2a", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "mark_applied", params: { opportunity_id: "job-1" } }) });
|
||||||
assert.equal(matchmakingResponse.status, 200);
|
assert.equal(matchmakingResponse.status, 200);
|
||||||
assert.ok(calls[0]?.url.endsWith("/a2a/tasks"));
|
assert.ok(calls[0]?.url.endsWith("/a2a/tasks"));
|
||||||
|
assert.equal(calls[0]?.headers.get("authorization"), config.a2aAllowedKey ? `Bearer ${config.a2aAllowedKey}` : null);
|
||||||
|
|
||||||
|
const forbiddenResponse = await app.request("http://backend.test/matchmaking/a2a", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "force_forbidden" }) });
|
||||||
|
assert.equal(forbiddenResponse.status, 502);
|
||||||
|
assert.match(await forbiddenResponse.text(), /matchmaking service authentication failed/);
|
||||||
|
|
||||||
|
const agentErrorResponse = await app.request("http://backend.test/matchmaking/a2a", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "agent_error" }) });
|
||||||
|
assert.equal(agentErrorResponse.status, 502);
|
||||||
|
assert.match(await agentErrorResponse.text(), /could not complete the search/);
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-10
@@ -389,17 +389,48 @@ async function callMatchmakingA2a(body: Record<string, unknown>, userId: string)
|
|||||||
action: action === "get_feed" ? "get_scout_feed" : action,
|
action: action === "get_feed" ? "get_scout_feed" : action,
|
||||||
user_id: getString(body.user_id) ?? userId,
|
user_id: getString(body.user_id) ?? userId,
|
||||||
};
|
};
|
||||||
const res = await fetch(target, {
|
const startedAt = Date.now();
|
||||||
method: "POST",
|
let res: Response;
|
||||||
headers: {
|
try {
|
||||||
"content-type": "application/json",
|
res = await fetch(target, {
|
||||||
...(config.a2aAllowedKey ? { authorization: `Bearer ${config.a2aAllowedKey}` } : {}),
|
method: "POST",
|
||||||
},
|
headers: {
|
||||||
body: JSON.stringify(payload),
|
"content-type": "application/json",
|
||||||
});
|
...(config.a2aAllowedKey ? { authorization: `Bearer ${config.a2aAllowedKey}` } : {}),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
log.warn({ err, service: "matchmaking", action, userId, durationMs: Date.now() - startedAt }, "matchmaking gateway request failed");
|
||||||
|
throw new HTTPException(502, { message: "matchmaking service is unavailable" });
|
||||||
|
}
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
const result = text ? JSON.parse(text) as Record<string, unknown> : {};
|
let result: Record<string, unknown> = {};
|
||||||
if (!res.ok) throw new HTTPException(res.status as never, { message: text || "matchmaking request failed" });
|
if (text) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
result = isRecord(parsed) ? parsed : { value: parsed };
|
||||||
|
} catch {
|
||||||
|
result = { detail: text };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
const upstreamAuthFailure = res.status === 401 || res.status === 403;
|
||||||
|
log.warn({
|
||||||
|
service: "matchmaking",
|
||||||
|
action,
|
||||||
|
userId,
|
||||||
|
upstreamStatus: res.status,
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
}, "matchmaking gateway upstream rejected request");
|
||||||
|
// A downstream service credential failure is infrastructure, not the
|
||||||
|
// browser user's auth. Returning 502 prevents an unnecessary Clerk token
|
||||||
|
// refresh/retry that would simply repeat the same rejected request.
|
||||||
|
throw new HTTPException((upstreamAuthFailure ? 502 : res.status) as never, {
|
||||||
|
message: upstreamAuthFailure ? "matchmaking service authentication failed" : text || "matchmaking request failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log.info({ service: "matchmaking", action, userId, upstreamStatus: res.status, durationMs: Date.now() - startedAt }, "matchmaking gateway request completed");
|
||||||
return { action: String(payload.action || action), result };
|
return { action: String(payload.action || action), result };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1227,6 +1258,10 @@ export function serviceRoutes(options: { skipAuth?: boolean } = {}) {
|
|||||||
const userId = c.get("userId");
|
const userId = c.get("userId");
|
||||||
const body = await c.req.json<JsonObject>().catch((): JsonObject => ({}));
|
const body = await c.req.json<JsonObject>().catch((): JsonObject => ({}));
|
||||||
const { result } = await callMatchmakingA2a({ ...body, user_id: userId }, userId);
|
const { result } = await callMatchmakingA2a({ ...body, user_id: userId }, userId);
|
||||||
|
if (resultHasAgentError(result)) {
|
||||||
|
log.warn({ service: "matchmaking", action: body.action, userId }, "matchmaking agent reported task failure");
|
||||||
|
throw new HTTPException(502, { message: "matchmaking service could not complete the search" });
|
||||||
|
}
|
||||||
// matchmaking-service emits canonical GrowEvents itself. The gateway is a
|
// matchmaking-service emits canonical GrowEvents itself. The gateway is a
|
||||||
// transport proxy only, avoiding a second event for the same action.
|
// transport proxy only, avoiding a second event for the same action.
|
||||||
return c.json(result);
|
return c.json(result);
|
||||||
|
|||||||
Reference in New Issue
Block a user