|
1 | | -import { describe, expect, it } from "vitest"; |
2 | | -import { consumerTasksForQueue, queueMetricsAreEmpty } from "./tool-api"; |
| 1 | +import { afterEach, describe, expect, it, vi } from "vitest"; |
| 2 | +import { |
| 3 | + buildApiTools, |
| 4 | + consumerTasksForQueue, |
| 5 | + pickQueueLiveState, |
| 6 | + queueMetricsAreEmpty, |
| 7 | + readQueueLiveState, |
| 8 | + withLiveState, |
| 9 | +} from "./tool-api"; |
| 10 | +import { createApiClient } from "./tool-api-client"; |
3 | 11 |
|
4 | 12 | /** |
5 | 13 | * The metrics route answers an unknown queue with zeroes rather than a 404, so asking for |
@@ -68,3 +76,116 @@ describe("consumerTasksForQueue", () => { |
68 | 76 | expect(consumerTasksForQueue({ worker: { tasks: [{}] } }, "email-sends")).toEqual([]); |
69 | 77 | }); |
70 | 78 | }); |
| 79 | + |
| 80 | +/** |
| 81 | + * A queue nobody can read is not a queue that isn't there. Only the route answering 404 is |
| 82 | + * evidence of absence; a 401, a 429 or a 5xx is evidence of nothing, and reporting one as |
| 83 | + * `exists: false` tells the model a queue holding thousands of runs was deleted. |
| 84 | + */ |
| 85 | +describe("the queue's live row has three answers, not two", () => { |
| 86 | + const metrics = { peakQueued: 4800, startedCount: 12 }; |
| 87 | + |
| 88 | + it("reads a row, a 404 and a failed read apart", () => { |
| 89 | + expect(readQueueLiveState({ ok: true, data: { paused: true } })).toEqual({ |
| 90 | + kind: "row", |
| 91 | + row: { paused: true }, |
| 92 | + }); |
| 93 | + expect(readQueueLiveState({ ok: false, status: 404 })).toEqual({ kind: "missing" }); |
| 94 | + for (const status of [401, 403, 429, 500, 503]) { |
| 95 | + expect(readQueueLiveState({ ok: false, status })).toEqual({ kind: "unknown", status }); |
| 96 | + } |
| 97 | + // No current environment: nothing was asked, so nothing is known. |
| 98 | + expect(readQueueLiveState(null)).toEqual({ kind: "unknown" }); |
| 99 | + }); |
| 100 | + |
| 101 | + it("says unknown rather than absent when the read failed", () => { |
| 102 | + expect(withLiveState(metrics, "custom", { kind: "unknown", status: 503 })).toMatchObject({ |
| 103 | + exists: "unknown", |
| 104 | + liveStateError: "Couldn't read the queue's live row (status 503).", |
| 105 | + }); |
| 106 | + expect(withLiveState(metrics, "custom", { kind: "missing" })).toMatchObject({ exists: false }); |
| 107 | + expect( |
| 108 | + withLiveState(metrics, "custom", { kind: "row", row: { paused: true, queued: 9 } }) |
| 109 | + ).toMatchObject({ exists: true, paused: true, queuedNow: 9 }); |
| 110 | + }); |
| 111 | + |
| 112 | + it("prefers a row, then a failed read, over a single 404", () => { |
| 113 | + const row = { kind: "row", row: { paused: false } } as const; |
| 114 | + const missing = { kind: "missing" } as const; |
| 115 | + const unknown = { kind: "unknown", status: 500 } as const; |
| 116 | + |
| 117 | + expect(pickQueueLiveState(missing, row)).toEqual(row); |
| 118 | + expect(pickQueueLiveState(unknown, row)).toEqual(row); |
| 119 | + // One kind 404s while the other read broke: that is not proof the name is free. |
| 120 | + expect(pickQueueLiveState(missing, unknown)).toEqual(unknown); |
| 121 | + expect(pickQueueLiveState(unknown, missing)).toEqual(unknown); |
| 122 | + expect(pickQueueLiveState(missing, missing)).toEqual(missing); |
| 123 | + }); |
| 124 | +}); |
| 125 | + |
| 126 | +/** The same three cases through `get_queue`, since the tool output is what the model reads. */ |
| 127 | +describe("get_queue reports the live read it actually got", () => { |
| 128 | + const ORIGIN = "https://api.example.com"; |
| 129 | + |
| 130 | + function stubFetch(liveResponse: () => Response) { |
| 131 | + vi.stubGlobal( |
| 132 | + "fetch", |
| 133 | + vi.fn(async (input: any) => { |
| 134 | + const url = typeof input === "string" ? input : input.url; |
| 135 | + if (url.endsWith("/jwt")) { |
| 136 | + return new Response(JSON.stringify({ token: "env-jwt" }), { status: 200 }); |
| 137 | + } |
| 138 | + if (url.includes("/metrics")) { |
| 139 | + return new Response(JSON.stringify({ peakQueued: 4800, startedCount: 12 }), { |
| 140 | + status: 200, |
| 141 | + }); |
| 142 | + } |
| 143 | + return liveResponse(); |
| 144 | + }) |
| 145 | + ); |
| 146 | + } |
| 147 | + |
| 148 | + function getQueue() { |
| 149 | + const ctx = { |
| 150 | + userActorToken: "uat", |
| 151 | + apiOrigin: ORIGIN, |
| 152 | + projectRef: "proj_ref", |
| 153 | + environmentName: "dev", |
| 154 | + }; |
| 155 | + const tools = buildApiTools({ |
| 156 | + ctx, |
| 157 | + client: createApiClient(ctx), |
| 158 | + renderInvestigations: (() => []) as any, |
| 159 | + }); |
| 160 | + return (input: any) => (tools.get_queue as any).execute(input, {} as any); |
| 161 | + } |
| 162 | + |
| 163 | + afterEach(() => vi.unstubAllGlobals()); |
| 164 | + |
| 165 | + it("reports a healthy row as the queue that exists", async () => { |
| 166 | + stubFetch( |
| 167 | + () => |
| 168 | + new Response(JSON.stringify({ type: "custom", paused: true, queued: 31 }), { status: 200 }) |
| 169 | + ); |
| 170 | + await expect(getQueue()({ queue: "email-sends", type: "custom" })).resolves.toMatchObject({ |
| 171 | + exists: true, |
| 172 | + paused: true, |
| 173 | + queuedNow: 31, |
| 174 | + }); |
| 175 | + }); |
| 176 | + |
| 177 | + it("reports a 404 as the queue that isn't there", async () => { |
| 178 | + stubFetch(() => new Response("", { status: 404 })); |
| 179 | + await expect(getQueue()({ queue: "email-sends", type: "custom" })).resolves.toMatchObject({ |
| 180 | + exists: false, |
| 181 | + }); |
| 182 | + }); |
| 183 | + |
| 184 | + it("reports a failed read as unknown, never as absent", async () => { |
| 185 | + stubFetch(() => new Response("", { status: 503 })); |
| 186 | + const answer = await getQueue()({ queue: "email-sends", type: "custom" }); |
| 187 | + expect(answer).toMatchObject({ exists: "unknown" }); |
| 188 | + expect(answer.exists).not.toBe(false); |
| 189 | + expect(answer.liveStateError).toContain("503"); |
| 190 | + }); |
| 191 | +}); |
0 commit comments