Skip to content
Closed
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
17 changes: 5 additions & 12 deletions src/lib/availability/availability-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Simple two-tier status: success (green) or failure (red)
*/

import { and, eq, inArray, isNull, type SQLWrapper, sql } from "drizzle-orm";
import { and, eq, inArray, isNotNull, isNull, type SQLWrapper, sql } from "drizzle-orm";
import { db } from "@/drizzle/db";
import { messageRequest, providers } from "@/drizzle/schema";
import type {
Expand Down Expand Up @@ -62,19 +62,12 @@ export class AvailabilityQueryValidationError extends Error {
}

/**
* 当前版本把“已终态”收敛为 `statusCode` 已落库。
*
* 已知限制:在当前异步写入/丢 patch 的极端场景,或未来新增了 `durationMs` / `errorMessage`
* 已落库、但 `statusCode` 仍为空且已稳定结束的写路径时,这些记录会被当前可用性统计排除。
* 届时应引入独立的 finalized 谓词,而不是直接放宽为 `durationMs IS NOT NULL`。
* 可用性监控用 `statusCode IS NOT NULL` 作为终态边界。
* 请求过程中的中间状态可能已经有 providerChain/errorMessage 片段,但仍不能算入可用性,
* 否则会把 requesting 中的请求误判为失败。success-rate outcome 仅用于终态记录的分类。
*/
function buildAvailabilityFinalizedCondition() {
return sql`fn_is_message_request_finalized(
${messageRequest.blockedBy},
${messageRequest.statusCode},
${messageRequest.providerChain},
${messageRequest.errorMessage}
)`;
return isNotNull(messageRequest.statusCode);
}
Comment thread
tesgth032 marked this conversation as resolved.
Comment thread
tesgth032 marked this conversation as resolved.

function assertValidDate(date: Date, fieldName: string): Date {
Expand Down
49 changes: 40 additions & 9 deletions tests/unit/lib/availability-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,45 @@ function normalizeSql(sqlObject: unknown): string {
return sqlToString(sqlObject).replace(/\s+/g, " ").trim().toLowerCase();
}

function findCteBoundary(
queryText: string,
cteName: string
): { cteStart: number; boundaryStart: number } | null {
// cteName 先经长度/格式校验,再生成 escapedName 参与受控测试正则构造。
if (queryText.length === 0 || cteName.length > 100 || !/^[a-z_][a-z0-9_]*$/i.test(cteName)) {
return null;
}

const escapedName = cteName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = new RegExp(`(^|\\bwith\\s+|,\\s*)("?${escapedName}"?)\\s+as\\s*\\(`).exec(
queryText
);
Comment on lines +47 to +54
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The findCteBoundary function performs a case-insensitive validation on cteName (using the /i flag on line 47), but the RegExp constructed on line 52 is case-sensitive. Since queryText is explicitly lowercased by normalizeSql, a cteName passed with uppercase characters would pass the initial validation but fail to match the query text. For consistency and robustness, the RegExp should also use the i flag, or cteName should be lowercased before constructing the regex.

Suggested change
if (queryText.length === 0 || cteName.length > 100 || !/^[a-z_][a-z0-9_]*$/i.test(cteName)) {
return null;
}
const escapedName = cteName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = new RegExp(`(^|\\bwith\\s+|,\\s*)("?${escapedName}"?)\\s+as\\s*\\(`).exec(
queryText
);
if (queryText.length === 0 || cteName.length > 100 || !/^[a-z_][a-z0-9_]*$/i.test(cteName)) {
return null;
}
const escapedName = cteName.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = new RegExp("(^|\\bwith\\s+|,\\s*)(\"?" + escapedName + "\"?)\\s+as\\s*\\(", "i").exec(
queryText
);


if (!match?.[2]) {
return null;
}

return {
cteStart: match.index + match[1].length,
boundaryStart: match.index,
};
}
Comment thread
tesgth032 marked this conversation as resolved.

function extractFinalizedRequestsSql(queryText: string): string {
const start = queryText.indexOf("finalized_requests as");
const end = queryText.indexOf("provider_bucket_stats as");
const start = findCteBoundary(queryText, "finalized_requests");
const end = findCteBoundary(queryText, "provider_bucket_stats");

if (start === -1 || end === -1 || end <= start) {
throw new Error("Could not locate finalized_requests CTE in query text");
if (!start) {
throw new Error("finalized_requests CTE not found in query text");
}
if (!end) {
throw new Error("provider_bucket_stats CTE not found in query text");
}
if (end.boundaryStart <= start.cteStart) {
throw new Error("finalized_requests CTE appears after provider_bucket_stats CTE");
}

return queryText.slice(start, end);
return queryText.slice(start.cteStart, end.boundaryStart);
}

describe("availability-service", () => {
Expand Down Expand Up @@ -309,7 +339,7 @@ describe("availability-service", () => {

const queryText = normalizeSql(executeMock.mock.calls[0]?.[0]);
const finalizedRequestsSql = extractFinalizedRequestsSql(queryText);
expect(finalizedRequestsSql).toContain("fn_is_message_request_finalized");
expect(finalizedRequestsSql).toContain('"status_code" is not null');
expect(queryText).toContain("group by");
expect(queryText).toContain("percentile_cont(0.95)");
expect(queryText).toContain("row_number() over");
Expand Down Expand Up @@ -482,7 +512,7 @@ describe("availability-service", () => {
const finalizedRequestsSql = extractFinalizedRequestsSql(
normalizeSql(executeMock.mock.calls[0]?.[0])
);
expect(finalizedRequestsSql).toContain("fn_is_message_request_finalized");
expect(finalizedRequestsSql).toContain('"status_code" is not null');
});

it("queryProviderAvailability 会保留 Gemini passthrough 终态(statusCode!=null 且 durationMs=null)", async () => {
Expand Down Expand Up @@ -515,6 +545,7 @@ describe("availability-service", () => {
const finalizedRequestsSql = extractFinalizedRequestsSql(
normalizeSql(executeMock.mock.calls[0]?.[0])
);
expect(finalizedRequestsSql).toContain('"status_code" is not null');
expect(finalizedRequestsSql).not.toMatch(/where .*duration_?ms.*is not null/);
});

Expand Down Expand Up @@ -548,7 +579,7 @@ describe("availability-service", () => {
const queryText = normalizeSql(executeMock.mock.calls[0]?.[0]);
const finalizedRequestsSql = extractFinalizedRequestsSql(queryText);

expect(finalizedRequestsSql).toContain("fn_is_message_request_finalized");
expect(finalizedRequestsSql).toContain('"status_code" is not null');
expect(queryText).toContain("fn_compute_message_request_success_rate_outcome");
expect(queryText).toContain(`"successrateoutcome" = 'failure'`);
});
Expand Down Expand Up @@ -717,7 +748,7 @@ describe("availability-service", () => {
]);

const queryText = normalizeSql(executeMock.mock.calls[0]?.[0]);
expect(queryText).toContain("fn_is_message_request_finalized");
expect(queryText).toContain('"status_code" is not null');
expect(queryText).toContain(">= now() - (15 * interval '1 minute')");
expect(queryText).toContain("<= now()");
expect(queryText).toContain("count(*) filter");
Expand Down
Loading