Files
Jyotisha/frontend/tests/database-consultation-session-capacity.test.ts
T
jesse-ux dcfc2f15af fix(consultation): count visible text only for session quota (BUG-732)
Replace append_consultation_question so the 200,000 quota sums only user-visible text, and add a 1,000,000-byte whole-JSON physical cap. Both still return session_full. Advisory lock, request_id idempotency, and the 200-message cap are unchanged.
2026-09-16 07:41:41 +08:00

278 lines
9.3 KiB
TypeScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { startPostgresFixture } from "./helpers/postgres-fixture.ts";
const runnerPath = fileURLToPath(
new URL("../scripts/db-migrate.mjs", import.meta.url),
);
function dockerAvailable(): boolean {
return spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], {
encoding: "utf8",
stdio: "ignore",
}).status === 0;
}
const skipWithoutDocker = dockerAvailable() ? false : "docker unavailable on this host";
const SESSION_ID = "99999999-9999-4999-8999-999999999999";
function sqlLiteral(value: string): string {
return value.replaceAll("'", "''");
}
function appendQuestionSql(
fixture: ReturnType<typeof startPostgresFixture>,
userId: string,
requestId: string,
textSql: string,
sessionId = SESSION_ID,
): string {
return fixture.psql(`
select coalesce(success::text, 'null') || ':' || coalesce(error_code, 'null')
from public.append_consultation_question(
'${userId}'::uuid,
'${sqlLiteral(requestId)}',
'${sessionId}'::uuid,
jsonb_build_object('role', 'user', 'text', ${textSql})
);
`);
}
function appendQuestion(
fixture: ReturnType<typeof startPostgresFixture>,
userId: string,
requestId: string,
text: string,
sessionId = SESSION_ID,
): string {
return appendQuestionSql(fixture, userId, requestId, `'${sqlLiteral(text)}'`, sessionId);
}
function seedSession(
fixture: ReturnType<typeof startPostgresFixture>,
userId: string,
messagesSql: string,
): void {
fixture.psql(`
insert into public.chat_sessions (
id, user_id, title, theme, model_id, messages, session_type, updated_at
) values (
'${SESSION_ID}', '${userId}', '新对话', 'general',
'test-model', ${messagesSql}, 'consultation', now()
)
on conflict (id) do update
set messages = excluded.messages,
title = excluded.title,
updated_at = now();
`);
}
test("append_consultation_question ignores thinking fields and enforces the physical JSON cap", { skip: skipWithoutDocker }, () => {
const fixture = startPostgresFixture();
const schemaUrl = fixture.connectionUrl("schema_owner", "schema-owner-test-password");
try {
const migration = spawnSync(process.execPath, [runnerPath], {
encoding: "utf8",
env: { ...process.env, SCHEMA_DATABASE_URL: schemaUrl },
});
assert.equal(migration.status, 0, migration.stderr);
assert.match(migration.stdout, /applied 20260916010000_consultation_session_capacity\.sql/);
fixture.psqlAs(
"identity_runtime",
"identity-runtime-test-password",
`
insert into identity.users (name, email, email_verified, email_verified_at)
values ('Capacity User', 'capacity@example.com', true, now());
`,
);
const userId = fixture.psql(
"select id from identity.users where email = 'capacity@example.com'",
);
assert.equal(
fixture.psql(`
select
has_function_privilege(
'service_role',
'public.append_consultation_question(uuid, text, uuid, jsonb)',
'execute'
) || ':' ||
has_function_privilege(
'authenticated',
'public.append_consultation_question(uuid, text, uuid, jsonb)',
'execute'
) || ':' ||
has_function_privilege(
'anon',
'public.append_consultation_question(uuid, text, uuid, jsonb)',
'execute'
)
`),
"true:f:f",
);
assert.equal(
appendQuestion(fixture, userId, "missing-session", "会话不存在"),
"false:session_missing",
);
seedSession(fixture, userId, "'[]'::jsonb");
assert.equal(appendQuestion(fixture, userId, "append-request-1", "第一问会不会丢"), "true:null");
assert.equal(appendQuestion(fixture, userId, "append-request-1", "不该写入的重复提问"), "true:null");
assert.equal(
fixture.psql(`select jsonb_array_length(messages) from public.chat_sessions where id = '${SESSION_ID}'`),
"1",
);
assert.equal(
appendQuestionSql(fixture, userId, "too-long", "repeat('x', 16001)"),
"false:invalid_question_message",
);
seedSession(fixture, userId, `
(
select coalesce(jsonb_agg(elem order by n), '[]'::jsonb)
from (
select n,
case when n % 2 = 1 then
jsonb_build_object(
'role', 'user',
'text', '短问题',
'requestId', 'think-seed-' || n::text
)
else
jsonb_build_object(
'role', 'assistant',
'text', '短回答',
'thinkingText', repeat('T', 4000),
'thinkingSections', jsonb_build_object('pad', repeat('S', 3000)),
'techniqueTruth', 'verified',
'workflowReceipt', jsonb_build_object(
'route', 'career',
'status', 'ready',
'preciseTiming', 'allowed',
'missingLayers', jsonb_build_array()
)
)
end as elem
from generate_series(1, 60) as n
) as seeded
)
`);
const thinkingSums = fixture.psql(`
select
coalesce(sum(length(coalesce(elem->>'text', ''))), 0)::text
|| ':' ||
coalesce(sum(
length(coalesce(elem->>'text', ''))
+ length(coalesce(elem->>'thinkingText', ''))
+ case when elem ? 'thinkingSections'
then length((elem->'thinkingSections')::text) else 0 end
), 0)::text
|| ':' ||
coalesce(sum(length(elem::text)), 0)::text
from public.chat_sessions,
jsonb_array_elements(coalesce(messages, '[]'::jsonb)) as elem
where id = '${SESSION_ID}'
`);
const [textOnly, oldFormula, physical] = thinkingSums.split(":").map(Number);
assert.ok(textOnly < 50_000, `visible text was ${textOnly}`);
assert.ok(oldFormula > 200_000, `old combined formula was ${oldFormula}`);
assert.ok(physical < 1_000_000, `physical JSON was ${physical}`);
assert.equal(appendQuestion(fixture, userId, "after-thinking", "思考不该占额度"), "true:null");
assert.equal(
fixture.psql(`select jsonb_array_length(messages) from public.chat_sessions where id = '${SESSION_ID}'`),
"61",
);
seedSession(fixture, userId, `
(
select coalesce(jsonb_agg(
jsonb_build_object('role', 'assistant', 'text', repeat('x', 19900), 'requestId', n::text)
order by n
), '[]'::jsonb)
from generate_series(1, 10) as n
)
`);
const quotaBoundary = fixture.psql(`
select
coalesce(sum(length(coalesce(elem->>'text', ''))), 0)::text
|| ':' ||
coalesce(sum(length(elem::text)), 0)::text
from public.chat_sessions,
jsonb_array_elements(coalesce(messages, '[]'::jsonb)) as elem
where id = '${SESSION_ID}'
`);
const [quotaText, quotaPhysical] = quotaBoundary.split(":").map(Number);
assert.equal(quotaText, 199_000);
assert.ok(quotaPhysical < 1_000_000, `quota-boundary physical JSON was ${quotaPhysical}`);
assert.equal(
appendQuestionSql(fixture, userId, "quota-full", "repeat('y', 1001)"),
"false:session_full",
);
assert.equal(
fixture.psql(`select jsonb_array_length(messages) from public.chat_sessions where id = '${SESSION_ID}'`),
"10",
);
seedSession(fixture, userId, `
jsonb_build_array(
jsonb_build_object(
'role', 'assistant',
'text', '短',
'techniqueTruth', 'verified',
'workflowReceipt', jsonb_build_object('route', 'career', 'status', 'ready', 'preciseTiming', 'allowed', 'missingLayers', jsonb_build_array()),
'agentExecutionReceipt', jsonb_build_object('pad', repeat('R', 1000001))
)
)
`);
const physicalOnly = fixture.psql(`
select
coalesce(sum(length(coalesce(elem->>'text', ''))), 0)::text
|| ':' ||
coalesce(sum(length(elem::text)), 0)::text
from public.chat_sessions,
jsonb_array_elements(coalesce(messages, '[]'::jsonb)) as elem
where id = '${SESSION_ID}'
`);
const [shortText, hugePhysical] = physicalOnly.split(":").map(Number);
assert.ok(shortText < 100, `short body was ${shortText}`);
assert.ok(hugePhysical > 1_000_000, `receipt pad physical JSON was ${hugePhysical}`);
assert.equal(
appendQuestion(fixture, userId, "physical-full", "正文很短但行已经胀了"),
"false:session_full",
);
assert.equal(
fixture.psql(`select jsonb_array_length(messages) from public.chat_sessions where id = '${SESSION_ID}'`),
"1",
);
seedSession(fixture, userId, `
(
select coalesce(jsonb_agg(
jsonb_build_object('role', 'user', 'text', 'x', 'requestId', n::text)
order by n
), '[]'::jsonb)
from generate_series(1, 200) as n
)
`);
assert.equal(
appendQuestion(fixture, userId, "count-full", "满了就不能再写"),
"false:session_full",
);
assert.equal(
fixture.psql(`select jsonb_array_length(messages) from public.chat_sessions where id = '${SESSION_ID}'`),
"200",
);
} finally {
fixture.stop();
}
});