From cb4f35260f15df61a67d21a6dd6fe09dd37a6922 Mon Sep 17 00:00:00 2001 From: ding113 Date: Mon, 3 Aug 2026 14:59:03 +0800 Subject: [PATCH 1/4] fix(message): restore canonical expression-index lookup for reserved identities The canonical session lookup for reserved identities had drifted away from the messageSessionIdentity expression index, causing queries to miss the optimised index path. The condition now always anchors on the expression-index column while still allowing owner-scoped lookups to match legacy null-identity rows via the session_id fallback. Unscoped reserved lookups remain narrow and do not pick up unrelated null-identity rows, preserving reserved identity isolation. --- src/repository/message.ts | 13 +++++++------ .../message-session-request-query.test.ts | 6 ++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/repository/message.ts b/src/repository/message.ts index 605614a31..e87db7e9b 100644 --- a/src/repository/message.ts +++ b/src/repository/message.ts @@ -77,12 +77,13 @@ function messageSessionLookup(identityOrPhysicalId: string, ownerUserId?: number function messageCanonicalSessionLookup(identity: string, ownerUserId?: number) { const canonicalCondition = isReservedSessionIdentity(identity) - ? ownerUserId !== undefined - ? or( - eq(messageRequest.sessionIdentity, identity), - and(isNull(messageRequest.sessionIdentity), eq(messageRequest.sessionId, identity)) - ) - : eq(messageRequest.sessionIdentity, identity) + ? and( + // 保留 expression index 入口,同时避免 reserved identity 混入同名物理 Session。 + eq(messageSessionIdentity, identity), + ownerUserId !== undefined + ? or(eq(messageRequest.sessionIdentity, identity), isNull(messageRequest.sessionIdentity)) + : eq(messageRequest.sessionIdentity, identity) + ) : eq(messageSessionIdentity, identity); return and( diff --git a/tests/unit/repository/message-session-request-query.test.ts b/tests/unit/repository/message-session-request-query.test.ts index c05a81e5c..dd419be3a 100644 --- a/tests/unit/repository/message-session-request-query.test.ts +++ b/tests/unit/repository/message-session-request-query.test.ts @@ -252,7 +252,7 @@ describe("message repository session request queries", () => { } }); - test("does not add the legacy physical fallback for unscoped reserved identities", async () => { + test("uses the canonical expression index without adding an unscoped physical fallback", async () => { const count = createDrizzleQuery([{ count: 1 }]); const rows = createDrizzleQuery([]); boundary.select.mockReturnValueOnce(count).mockReturnValueOnce(rows); @@ -260,8 +260,10 @@ describe("message repository session request queries", () => { await findRequestsBySessionIdentity("pfx:canonical", {} as never); for (const where of [sqlText(count.trace.where), sqlText(rows.trace.where)]) { + expect(where).toContain("coalesce"); + expect(where).toContain("session_identity"); expect(where).not.toContain("session_identity is null"); - expect(where.match(/pfx:canonical/g)).toHaveLength(1); + expect(where.match(/pfx:canonical/g)).toHaveLength(2); } }); From 3ecc71b5a1e87027eaf398efdd7f3863a459825f Mon Sep 17 00:00:00 2001 From: ding113 Date: Mon, 3 Aug 2026 15:02:44 +0800 Subject: [PATCH 2/4] test(repository): assert coalesce in owner-scoped legacy identity fallback Extend the reserved identity SQL contract test to verify that the generated where clause uses coalesce when resolving owner-scoped session requests for legacy clients. --- tests/unit/repository/message-session-request-query.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/repository/message-session-request-query.test.ts b/tests/unit/repository/message-session-request-query.test.ts index dd419be3a..d4e16ed3e 100644 --- a/tests/unit/repository/message-session-request-query.test.ts +++ b/tests/unit/repository/message-session-request-query.test.ts @@ -245,6 +245,7 @@ describe("message repository session request queries", () => { await findRequestsBySessionIdentity("pfx:legacy-client", { ownerUserId: 17 } as never); for (const where of [sqlText(count.trace.where), sqlText(rows.trace.where)]) { + expect(where).toContain("coalesce"); expect(where).toContain("user_id"); expect(where).toContain("is null"); expect(where).toContain("session_id"); From 535042ac352de5b01bb37e72c71ca6bf0b3e2aed Mon Sep 17 00:00:00 2001 From: ding113 Date: Mon, 3 Aug 2026 15:23:19 +0800 Subject: [PATCH 3/4] test(sessions): strengthen reserved identity query and route coverage Add encoded-identity regression cases to the v1 session requests route test, verifying that URL-encoded session identities (pfx: and sid: prefixed) resolve correctly through the endpoint. Rewrite the repository-level session request query tests to compile the generated SQL via PgDialect instead of string matching, and parameterize them across both pfx: and sid: identity prefixes. The assertions now verify the exact coalesce expression, identity guards, and parameter bindings for both owner-scoped legacy fallback and unscoped canonical lookups. --- tests/api/v1/sessions/sessions.test.ts | 10 +++ .../message-session-request-query.test.ts | 77 ++++++++++++------- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/tests/api/v1/sessions/sessions.test.ts b/tests/api/v1/sessions/sessions.test.ts index 2b89cac21..c1e3cff3c 100644 --- a/tests/api/v1/sessions/sessions.test.ts +++ b/tests/api/v1/sessions/sessions.test.ts @@ -137,6 +137,16 @@ describe("v1 session endpoints", () => { expect(requests.response.status).toBe(200); expect(getSessionRequestsMock).toHaveBeenCalledWith("s1", 2, 5, "desc"); + for (const identity of ["pfx:scope:fingerprint", "sid:canonical-session"]) { + const encodedRequests = await callV1Route({ + method: "GET", + pathname: `/api/v1/sessions/${encodeURIComponent(identity)}/requests?page=1&pageSize=20&order=desc`, + headers, + }); + expect(encodedRequests.response.status).toBe(200); + expect(getSessionRequestsMock).toHaveBeenLastCalledWith(identity, 1, 20, "desc"); + } + await callV1Route({ method: "GET", pathname: "/api/v1/sessions/s1/requests", diff --git a/tests/unit/repository/message-session-request-query.test.ts b/tests/unit/repository/message-session-request-query.test.ts index d4e16ed3e..530280233 100644 --- a/tests/unit/repository/message-session-request-query.test.ts +++ b/tests/unit/repository/message-session-request-query.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; +import { PgDialect } from "drizzle-orm/pg-core"; import { messageRequest } from "@/drizzle/schema"; import { keys as keysTable } from "@/drizzle/schema"; import { @@ -50,6 +51,12 @@ type RequestRow = Pick< const firstCreatedAt = new Date("2026-05-04T10:00:00.000Z"); const secondCreatedAt = new Date("2026-05-04T10:01:00.000Z"); +const dialect = new PgDialect(); + +function compileWhere(values: readonly unknown[]) { + const query = dialect.sqlToQuery(values.at(0) as never); + return { sql: query.sql.toLowerCase(), params: query.params }; +} describe("message repository session request queries", () => { beforeEach(() => { @@ -237,36 +244,50 @@ describe("message repository session request queries", () => { expect(rowsWhere.match(/shared-session/g)).toHaveLength(1); }); - test("includes a legacy null-identity physical fallback for owner-scoped reserved identities", async () => { - const count = createDrizzleQuery([{ count: 1 }]); - const rows = createDrizzleQuery([]); - boundary.select.mockReturnValueOnce(count).mockReturnValueOnce(rows); - - await findRequestsBySessionIdentity("pfx:legacy-client", { ownerUserId: 17 } as never); - - for (const where of [sqlText(count.trace.where), sqlText(rows.trace.where)]) { - expect(where).toContain("coalesce"); - expect(where).toContain("user_id"); - expect(where).toContain("is null"); - expect(where).toContain("session_id"); - expect(where.match(/pfx:legacy-client/g)).toHaveLength(2); + test.each(["pfx:legacy-client", "sid:legacy-client"])( + "includes an owner-scoped legacy fallback without aliasing a non-null identity: %s", + async (identity) => { + const count = createDrizzleQuery([{ count: 1 }]); + const rows = createDrizzleQuery([]); + boundary.select.mockReturnValueOnce(count).mockReturnValueOnce(rows); + + await findRequestsBySessionIdentity(identity, { ownerUserId: 17 } as never); + + for (const where of [count.trace.where, rows.trace.where]) { + const compiled = compileWhere(where); + expect(compiled.sql).toContain( + 'coalesce("message_request"."session_identity", "message_request"."session_id") =' + ); + expect(compiled.sql).toContain( + '("message_request"."session_identity" = $2 or "message_request"."session_identity" is null)' + ); + expect(compiled.sql).not.toContain('or "message_request"."session_id" ='); + expect(compiled.sql).toContain('"message_request"."user_id" = $3'); + expect(compiled.params.slice(0, 3)).toEqual([identity, identity, 17]); + } } - }); - - test("uses the canonical expression index without adding an unscoped physical fallback", async () => { - const count = createDrizzleQuery([{ count: 1 }]); - const rows = createDrizzleQuery([]); - boundary.select.mockReturnValueOnce(count).mockReturnValueOnce(rows); - - await findRequestsBySessionIdentity("pfx:canonical", {} as never); - - for (const where of [sqlText(count.trace.where), sqlText(rows.trace.where)]) { - expect(where).toContain("coalesce"); - expect(where).toContain("session_identity"); - expect(where).not.toContain("session_identity is null"); - expect(where.match(/pfx:canonical/g)).toHaveLength(2); + ); + + test.each(["pfx:canonical", "sid:canonical"])( + "uses the canonical expression index with an explicit unscoped identity guard: %s", + async (identity) => { + const count = createDrizzleQuery([{ count: 1 }]); + const rows = createDrizzleQuery([]); + boundary.select.mockReturnValueOnce(count).mockReturnValueOnce(rows); + + await findRequestsBySessionIdentity(identity, {} as never); + + for (const where of [count.trace.where, rows.trace.where]) { + const compiled = compileWhere(where); + expect(compiled.sql).toContain( + 'coalesce("message_request"."session_identity", "message_request"."session_id") = $1 and "message_request"."session_identity" = $2' + ); + expect(compiled.sql).not.toContain('"message_request"."session_identity" is null'); + expect(compiled.sql).not.toContain('or "message_request"."session_id" ='); + expect(compiled.params.slice(0, 2)).toEqual([identity, identity]); + } } - }); + ); test("does not treat a reserved canonical identity as a physical Session alias", async () => { const locator = createDrizzleQuery([ From 9696f449dd78c17ab9bdc30b9cecf522d5391b10 Mon Sep 17 00:00:00 2001 From: ding113 Date: Mon, 3 Aug 2026 15:39:22 +0800 Subject: [PATCH 4/4] style(message): normalize comment punctuation to half-width comma --- src/repository/message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repository/message.ts b/src/repository/message.ts index e87db7e9b..60901edad 100644 --- a/src/repository/message.ts +++ b/src/repository/message.ts @@ -78,7 +78,7 @@ function messageSessionLookup(identityOrPhysicalId: string, ownerUserId?: number function messageCanonicalSessionLookup(identity: string, ownerUserId?: number) { const canonicalCondition = isReservedSessionIdentity(identity) ? and( - // 保留 expression index 入口,同时避免 reserved identity 混入同名物理 Session。 + // 保留 expression index 入口, 同时避免 reserved identity 混入同名物理 Session. eq(messageSessionIdentity, identity), ownerUserId !== undefined ? or(eq(messageRequest.sessionIdentity, identity), isNull(messageRequest.sessionIdentity))