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.
278 lines
9.3 KiB
TypeScript
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();
|
|
}
|
|
});
|