One row per rectification Case, written once when the range card is first delivered (GET /api/rectification/cases/[caseId], fire-and-forget after the response is built). Numbers and closed enums only: no user / case / session id, birth data, names, text or timestamps finer than the ISO week. Dedupe via a separate case_id ledger that cascades with the Case (and account deletion). Migration 20260926010000 is additive: two RLS tables with no runtime table grants, SECURITY DEFINER write (service_role), purge (service_role) and aggregate-only summary (admin_runtime) functions; 180-day retention. Admin: 「校正统计」 page + GET /api/admin/rectification-telemetry (admin.customers.read), aggregates only, no per-row view or export. TASK-rectification-telemetry-20260926. test:db not run locally (no Docker). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
1151 lines
46 KiB
TypeScript
1151 lines
46 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { spawnSync } from "node:child_process";
|
||
import { existsSync, readFileSync } from "node:fs";
|
||
import { fileURLToPath } from "node:url";
|
||
import test from "node:test";
|
||
|
||
import {
|
||
closeLocalPostgresDataPools,
|
||
createLocalPostgresDataClient,
|
||
} from "../src/lib/db/local-postgres-client-core.ts";
|
||
import { loadLatestAgenticRectificationResult } from "../src/lib/rectification-agentic/session.ts";
|
||
import { startPostgresFixture } from "./helpers/postgres-fixture.ts";
|
||
|
||
const runnerPath = fileURLToPath(
|
||
new URL("../scripts/db-migrate.mjs", import.meta.url),
|
||
);
|
||
const acceptedExactFamilyMigration = readFileSync(
|
||
new URL("../supabase/migrations/20260816010000_accept_exact_family_birth_times.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const pinArchiveMigration = readFileSync(
|
||
new URL("../supabase/migrations/20260901020000_chat_session_pin_archive.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
|
||
function rpcError(error: unknown): string {
|
||
if (!error || typeof error !== "object") return "";
|
||
const value = error as { message?: unknown };
|
||
return typeof value.message === "string" ? value.message : "";
|
||
}
|
||
|
||
function candidateIds(value: unknown): string[] {
|
||
if (!value || typeof value !== "object") return [];
|
||
const candidates = (value as { candidates?: unknown }).candidates;
|
||
if (!Array.isArray(candidates)) return [];
|
||
return candidates.flatMap((candidate) => {
|
||
if (!candidate || typeof candidate !== "object") return [];
|
||
const candidateId = (candidate as { candidate_id?: unknown }).candidate_id;
|
||
return typeof candidateId === "string" ? [candidateId] : [];
|
||
});
|
||
}
|
||
|
||
test("local PostgreSQL applies the reviewed business schema and serves authenticated business calls", async () => {
|
||
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 20260715000000_account_credits\.sql/);
|
||
assert.match(migration.stdout, /applied 20260721150000_align_conversational_finance_domain\.sql/);
|
||
assert.match(migration.stdout, /applied 20260723010000_restore_conversational_message_history\.sql/);
|
||
assert.match(migration.stdout, /applied 20260723020000_mark_captured_conversational_messages\.sql/);
|
||
assert.match(migration.stdout, /applied 20260728010000_conversational_event_semantics\.sql/);
|
||
assert.match(migration.stdout, /applied 20260728020000_rectification_agent_v5\.sql/);
|
||
assert.match(migration.stdout, /applied 20260804010000_agentic_rectification_candidate_acceptance\.sql/);
|
||
assert.match(migration.stdout, /applied 20260804020000_preserve_reported_birth_time_on_candidate_acceptance\.sql/);
|
||
assert.match(migration.stdout, /applied 20260805010000_reconcile_admin_redemption_audit\.sql/);
|
||
assert.match(migration.stdout, /applied 20260805020000_reconcile_payment_admin_schema\.sql/);
|
||
assert.match(migration.stdout, /applied 20260805030000_reconcile_rectification_v4_conversational_turns\.sql/);
|
||
assert.match(migration.stdout, /applied 20260806000000_personal_reports\.sql/);
|
||
assert.match(migration.stdout, /applied 20260806010000_admin_rbac\.sql/);
|
||
assert.match(migration.stdout, /applied 20260806020000_billing_products_subscriptions\.sql/);
|
||
assert.match(migration.stdout, /applied 20260806030000_settle_order_usage_authorization\.sql/);
|
||
assert.match(migration.stdout, /applied 20260806040000_model_configuration\.sql/);
|
||
assert.match(migration.stdout, /applied 20260806050000_operations_feature_flags\.sql/);
|
||
assert.match(migration.stdout, /applied 20260811010000_consultation_status_service_role_read\.sql/);
|
||
assert.match(migration.stdout, /applied 20260812010000_agentic_rectification_v9_runtime\.sql/);
|
||
assert.match(migration.stdout, /applied 20260813010000_agentic_rectification_v9_agent_api\.sql/);
|
||
assert.match(migration.stdout, /applied 20260813060000_rectification_turn_regeneration\.sql/);
|
||
assert.match(migration.stdout, /applied 20260814020000_rectification_v10_runtime\.sql/);
|
||
assert.match(migration.stdout, /applied 20260814025000_personal_report_document_v2\.sql/);
|
||
assert.match(migration.stdout, /applied 20260814040000_personal_report_jobs_v2\.sql/);
|
||
assert.match(migration.stdout, /applied 20260816010000_accept_exact_family_birth_times\.sql/);
|
||
assert.match(migration.stdout, /applied 20260818010000_admin_product_catalog_mutations\.sql/);
|
||
assert.match(migration.stdout, /applied 20260819010000_rectification_ingest_precision_plateau\.sql/);
|
||
assert.match(migration.stdout, /applied 20260822010000_declared_birth_window_clocks\.sql/);
|
||
assert.match(migration.stdout, /applied 20260823010000_rectification_occupation_dateless\.sql/);
|
||
assert.match(migration.stdout, /applied 20260823020000_rectification_inference_choice_write\.sql/);
|
||
assert.match(migration.stdout, /applied 20260824010000_rectification_inference_transition_ledger\.sql/);
|
||
assert.match(migration.stdout, /applied 20260824020000_rectification_choice_action\.sql/);
|
||
assert.match(migration.stdout, /applied 20260824030000_rectification_turn_origin\.sql/);
|
||
assert.match(migration.stdout, /applied 20260831020000_feature_pricing_admin_runtime_read_policy\.sql/);
|
||
assert.match(migration.stdout, /applied 20260901010000_append_consultation_question\.sql/);
|
||
assert.match(migration.stdout, /applied 20260901020000_chat_session_pin_archive\.sql/);
|
||
assert.match(migration.stdout, /applied 20260905010000_personal_report_longform_appendices\.sql/);
|
||
assert.match(migration.stdout, /applied 20260915010000_rectification_touch_chat_session\.sql/);
|
||
assert.match(migration.stdout, /applied 20260916010000_consultation_session_capacity\.sql/);
|
||
assert.equal(
|
||
existsSync(fileURLToPath(new URL("../db/migrations/20260901020000_chat_session_pin_archive.sql", import.meta.url))),
|
||
false,
|
||
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
||
);
|
||
assert.equal(
|
||
existsSync(fileURLToPath(new URL("../db/migrations/20260905010000_personal_report_longform_appendices.sql", import.meta.url))),
|
||
false,
|
||
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
||
);
|
||
assert.equal(
|
||
existsSync(fileURLToPath(new URL("../db/migrations/20260916010000_consultation_session_capacity.sql", import.meta.url))),
|
||
false,
|
||
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
||
);
|
||
fixture.psql(pinArchiveMigration);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select is_nullable || ':' || data_type
|
||
from information_schema.columns
|
||
where table_schema = 'public'
|
||
and table_name = 'chat_sessions'
|
||
and column_name = 'pinned'
|
||
`),
|
||
"NO:boolean",
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select is_nullable || ':' || data_type
|
||
from information_schema.columns
|
||
where table_schema = 'public'
|
||
and table_name = 'chat_sessions'
|
||
and column_name = 'archived_at'
|
||
`),
|
||
"YES:timestamp with time zone",
|
||
);
|
||
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select is_nullable || ':' || data_type
|
||
from information_schema.columns
|
||
where table_schema = 'public'
|
||
and table_name = 'birth_time_rectification_turns'
|
||
and column_name = 'user_message'
|
||
`),
|
||
"YES:text",
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select is_nullable || ':' || data_type || ':' || column_default
|
||
from information_schema.columns
|
||
where table_schema = 'public'
|
||
and table_name = 'birth_time_rectification_turns'
|
||
and column_name = 'user_message_captured'
|
||
`),
|
||
"NO:boolean:f",
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select
|
||
has_function_privilege(
|
||
'service_role',
|
||
'public.load_conversational_rectification_case_with_history(uuid, uuid)',
|
||
'execute'
|
||
) || ':' ||
|
||
has_function_privilege(
|
||
'authenticated',
|
||
'public.load_conversational_rectification_case_with_history(uuid, uuid)',
|
||
'execute'
|
||
)
|
||
`),
|
||
"true:f",
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select
|
||
has_table_privilege('service_role', 'public.consultation_requests', 'select') || ':' ||
|
||
has_table_privilege('anon', 'public.consultation_requests', 'select') || ':' ||
|
||
has_table_privilege('authenticated', 'public.consultation_requests', 'select')
|
||
`),
|
||
"true:f:f",
|
||
);
|
||
|
||
fixture.psql(`
|
||
insert into public.feature_pricing(feature_key, model_tier, credit_cost, version)
|
||
values ('rectification', 'standard', 3, 1)
|
||
`);
|
||
assert.equal(
|
||
fixture.psqlAs(
|
||
"admin_runtime",
|
||
"admin-runtime-test-password",
|
||
"select feature_key || ':' || model_tier || ':' || credit_cost || ':' || status from public.feature_pricing",
|
||
),
|
||
"rectification:standard:3:draft",
|
||
);
|
||
|
||
// Public table allowlist. Last change (BUG-548):
|
||
// original: omitted personal_report_longform_appendices
|
||
// new: insert it between personal_report_jobs and personal_report_sections
|
||
// reason: 20260905010000 added the appendix cache table; this list must match live pg_tables
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select string_agg(tablename, ',' order by tablename)
|
||
from pg_tables
|
||
where schemaname = 'public'
|
||
`),
|
||
[
|
||
"admin_permissions",
|
||
"admin_role_permissions",
|
||
"admin_roles",
|
||
"admin_session_revocations",
|
||
"admin_user_roles",
|
||
"admin_users",
|
||
"agentic_rectification_candidate_decisions",
|
||
"agentic_rectification_candidates",
|
||
"agentic_rectification_case_conversation_summaries",
|
||
"agentic_rectification_cases",
|
||
"agentic_rectification_choice_actions",
|
||
"agentic_rectification_conversation_focuses",
|
||
"agentic_rectification_evidence",
|
||
"agentic_rectification_inference_transitions",
|
||
"agentic_rectification_open_ledger",
|
||
"agentic_rectification_results",
|
||
"agentic_rectification_run_attempts",
|
||
"agentic_rectification_run_phases",
|
||
"agentic_rectification_skill_run_receipts",
|
||
"agentic_rectification_skill_upgrade_receipts",
|
||
"agentic_rectification_tool_receipts",
|
||
"agentic_rectification_turn_regenerations",
|
||
"agentic_rectification_turns",
|
||
"billing_products",
|
||
"birth_time_rectification_action_receipts",
|
||
"birth_time_rectification_agent_runs",
|
||
"birth_time_rectification_billing",
|
||
"birth_time_rectification_candidate_feature_snapshots",
|
||
"birth_time_rectification_cases",
|
||
"birth_time_rectification_diagnostics",
|
||
"birth_time_rectification_dynamic_state",
|
||
"birth_time_rectification_event_evidence",
|
||
"birth_time_rectification_handoff_attach_receipts",
|
||
"birth_time_rectification_handoff_settlements",
|
||
"birth_time_rectification_pending_evidence",
|
||
"birth_time_rectification_public_messages",
|
||
"birth_time_rectification_question_handoffs",
|
||
"birth_time_rectification_scoring_jobs",
|
||
"birth_time_rectification_turns",
|
||
"birth_time_rectification_v4_actions",
|
||
"birth_time_rectification_v4_candidate_snapshots",
|
||
"birth_time_rectification_v4_cases",
|
||
"birth_time_rectification_v4_event_revisions",
|
||
"birth_time_rectification_v4_events",
|
||
"birth_time_rectification_v4_handoff_attach_receipts",
|
||
"birth_time_rectification_v4_handoff_settlements",
|
||
"birth_time_rectification_v4_handoffs",
|
||
"birth_time_rectification_v4_jobs",
|
||
"birth_time_rectification_v4_turns",
|
||
"chart_profiles",
|
||
"chat_sessions",
|
||
"consultation_requests",
|
||
"credit_request_cancellations",
|
||
"credit_transactions",
|
||
"epay_settings",
|
||
"feature_flags",
|
||
// This migration intentionally adds the server-owned feature pricing table.
|
||
"feature_pricing",
|
||
"model_config_versions",
|
||
"model_configs",
|
||
"model_connection_test_evidence",
|
||
"model_providers",
|
||
"model_publish_events",
|
||
"notification_templates",
|
||
"payment_orders",
|
||
"payment_packages",
|
||
"personal_report_jobs",
|
||
"personal_report_longform_appendices",
|
||
"personal_report_sections",
|
||
"personal_reports",
|
||
"pricing_experiment_events",
|
||
"product_entitlements",
|
||
"profiles",
|
||
// 原值: profiles 之后直接是 redemption_attempts
|
||
// 新值: 中间加入 rectification_telemetry 与 rectification_telemetry_reported_cases
|
||
// 原因: 20260926010000 加了匿名校正统计表与去重台账(TASK-rectification-telemetry-20260926)
|
||
"rectification_telemetry",
|
||
"rectification_telemetry_reported_cases",
|
||
"redemption_attempts",
|
||
"redemption_codes",
|
||
"synastry_reports",
|
||
"usage_events",
|
||
"usage_ledger",
|
||
"usage_reservations",
|
||
"user_product_redemptions",
|
||
"user_subscriptions",
|
||
].join(","),
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select string_agg(column_name, ',' order by column_name)
|
||
from information_schema.columns
|
||
where table_schema = 'public'
|
||
and table_name = 'agentic_rectification_turns'
|
||
and column_name in ('client_action_id', 'content_hash', 'message_origin')
|
||
`),
|
||
"client_action_id,content_hash,message_origin",
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select
|
||
has_function_privilege(
|
||
'service_role',
|
||
'public.record_agentic_rectification_turn_origin(uuid, uuid, uuid, text, uuid, text)',
|
||
'execute'
|
||
) || ':' ||
|
||
has_function_privilege(
|
||
'authenticated',
|
||
'public.record_agentic_rectification_turn_origin(uuid, uuid, uuid, text, uuid, text)',
|
||
'execute'
|
||
)
|
||
`),
|
||
"true:f",
|
||
);
|
||
|
||
fixture.psqlAs(
|
||
"identity_runtime",
|
||
"identity-runtime-test-password",
|
||
`
|
||
insert into identity.users (name, email, email_verified, email_verified_at)
|
||
values ('Local User', 'local-user@example.com', true, now())
|
||
`,
|
||
);
|
||
const userId = fixture.psql(
|
||
"select id from identity.users where email = 'local-user@example.com'",
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`select email from auth.users where id = '${userId}'`),
|
||
"local-user@example.com",
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`select email || ':' || credits from public.profiles where id = '${userId}'`),
|
||
"local-user@example.com:0",
|
||
);
|
||
assert.equal(
|
||
fixture.psqlAs(
|
||
"app_runtime",
|
||
"app-runtime-test-password",
|
||
`set role authenticated;
|
||
select set_config('request.jwt.claim.sub', '${userId}', true);
|
||
select email from public.profiles where id = '${userId}'`,
|
||
),
|
||
`SET\n${userId}\nlocal-user@example.com`,
|
||
);
|
||
fixture.psql(`update public.profiles set birth_date = '1997-08-08' where id = '${userId}'`);
|
||
fixture.psql(`
|
||
update public.profiles
|
||
set reported_birth_time = '05:00',
|
||
active_birth_time = null,
|
||
birth_time_source = 'family_exact',
|
||
uncertainty_before_minutes = 0,
|
||
uncertainty_after_minutes = 0,
|
||
birth_time_status = 'reported',
|
||
rectification_case_id = null
|
||
where id = '${userId}'
|
||
`);
|
||
fixture.psql(acceptedExactFamilyMigration);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select to_char(reported_birth_time, 'HH24:MI') || ':' ||
|
||
to_char(active_birth_time, 'HH24:MI') || ':' || birth_time_status
|
||
from public.profiles
|
||
where id = '${userId}'
|
||
`),
|
||
"05:00:05:00:accepted",
|
||
);
|
||
fixture.psql(`
|
||
update public.profiles
|
||
set reported_birth_time = '05:00',
|
||
active_birth_time = null,
|
||
birth_time_source = 'family_exact',
|
||
uncertainty_before_minutes = 10,
|
||
uncertainty_after_minutes = 10,
|
||
birth_time_status = 'reported',
|
||
rectification_case_id = null
|
||
where id = '${userId}'
|
||
`);
|
||
fixture.psql(acceptedExactFamilyMigration);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select active_birth_time is null || ':' || birth_time_status
|
||
from public.profiles
|
||
where id = '${userId}'
|
||
`),
|
||
"true:reported",
|
||
);
|
||
fixture.psql(`
|
||
update public.profiles
|
||
set reported_birth_time = null,
|
||
active_birth_time = null,
|
||
birth_time_source = null,
|
||
uncertainty_before_minutes = null,
|
||
uncertainty_after_minutes = null,
|
||
birth_time_status = null,
|
||
rectification_case_id = null
|
||
where id = '${userId}'
|
||
`);
|
||
|
||
const local = createLocalPostgresDataClient(
|
||
fixture.connectionUrl("app_runtime", "app-runtime-test-password"),
|
||
{ id: userId, email: "local-user@example.com" },
|
||
);
|
||
const profile = await local.from("profiles")
|
||
.select("id,email,credits,birth_date,created_at")
|
||
.eq("id", userId)
|
||
.single();
|
||
assert.equal(profile.error, null);
|
||
const { created_at: createdAt, ...profileData } = profile.data as Record<string, unknown>;
|
||
assert.equal(typeof createdAt, "string");
|
||
assert.match(String(createdAt), /^\d{4}-\d{2}-\d{2}T/);
|
||
assert.deepEqual(profileData, {
|
||
id: userId,
|
||
email: "local-user@example.com",
|
||
credits: 0,
|
||
birth_date: "1997-08-08",
|
||
});
|
||
const nonAbandonedProfiles = await local.from("profiles")
|
||
.select("id")
|
||
.neq("email", "not-local-user@example.com")
|
||
.single();
|
||
assert.equal(nonAbandonedProfiles.error, null);
|
||
assert.deepEqual(nonAbandonedProfiles.data, { id: userId });
|
||
const admin = createLocalPostgresDataClient(
|
||
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
|
||
null,
|
||
"service_role",
|
||
);
|
||
const adminProfile = await admin.from("profiles")
|
||
.select("id")
|
||
.eq("id", userId)
|
||
.single();
|
||
assert.equal(adminProfile.error, null);
|
||
assert.deepEqual(adminProfile.data, { id: userId });
|
||
|
||
const serviceScalar = (sql: string) => {
|
||
const output = fixture.psqlAs(
|
||
"service_runtime",
|
||
"service-runtime-test-password",
|
||
`set role service_role; ${sql}`,
|
||
);
|
||
return output.split("\n").at(-1) ?? "";
|
||
};
|
||
|
||
const terminalCaseId = "44444444-4444-4444-8444-444444444444";
|
||
const terminalSessionId = "55555555-5555-4555-8555-555555555555";
|
||
const terminalRequestId = "77777777-7777-4777-8777-777777777777";
|
||
const skillSha256 = "a".repeat(64);
|
||
const skillSourceCommit = "b".repeat(40);
|
||
fixture.psql(`
|
||
insert into public.chat_sessions (
|
||
id, user_id, title, theme, model_id, messages, session_type, updated_at
|
||
) values (
|
||
'${terminalSessionId}', '${userId}', 'V10 terminal finalize', 'general',
|
||
'test-model', '[]', 'birth_time_rectification', now()
|
||
);
|
||
insert into public.agentic_rectification_cases (
|
||
id, user_id, session_id, status, skill_name, skill_version,
|
||
skill_sha256, skill_source_commit, baseline_profile_fingerprint,
|
||
baseline_birth_snapshot, candidate_range
|
||
) values (
|
||
'${terminalCaseId}', '${userId}', '${terminalSessionId}', 'collecting_evidence',
|
||
'jyotish-birth-time-rectification', '10.0.0', '${skillSha256}',
|
||
'${skillSourceCommit}', 'v10-terminal-fixture', '{}', '{}'
|
||
);
|
||
`);
|
||
|
||
const terminalTurnId = serviceScalar(`
|
||
select result ->> 'turn_id'
|
||
from (
|
||
select public.append_agentic_rectification_turn(
|
||
'${userId}', '${terminalCaseId}', '确认这个时间', null,
|
||
'test-model', null, 'pending', '${terminalRequestId}'
|
||
) as result
|
||
) appended
|
||
`);
|
||
assert.match(terminalTurnId, /^[0-9a-f-]{36}$/);
|
||
assert.equal(
|
||
serviceScalar(`
|
||
select (result ->> 'turn_id') || ':' ||
|
||
(result ->> 'should_execute') || ':' ||
|
||
(result ->> 'idempotent')
|
||
from (
|
||
select public.append_agentic_rectification_turn(
|
||
'${userId}', '${terminalCaseId}', '确认这个时间', null,
|
||
'test-model', null, 'pending', '${terminalRequestId}'
|
||
) as result
|
||
) replayed
|
||
`),
|
||
`${terminalTurnId}:false:true`,
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select count(*)
|
||
from public.agentic_rectification_turns
|
||
where case_id = '${terminalCaseId}' and request_id = '${terminalRequestId}'
|
||
`),
|
||
"1",
|
||
);
|
||
assert.throws(
|
||
() => serviceScalar(`
|
||
select public.append_agentic_rectification_turn(
|
||
'${userId}', '${terminalCaseId}', '不同的请求正文', null,
|
||
'test-model', null, 'pending', '${terminalRequestId}'
|
||
)
|
||
`),
|
||
/agentic_rectification_request_mismatch/,
|
||
);
|
||
for (const legacySignature of [
|
||
"public.append_agentic_rectification_turn(uuid,uuid,text,text,text,text,text)",
|
||
"public.finalize_agentic_rectification_turn(uuid,uuid,uuid,text,text)",
|
||
"public.finalize_agentic_rectification_turn(uuid,uuid,uuid,text,text,uuid)",
|
||
]) {
|
||
assert.equal(
|
||
fixture.psql(`select pg_catalog.has_function_privilege('service_role', '${legacySignature}', 'EXECUTE')`),
|
||
"f",
|
||
);
|
||
}
|
||
|
||
const terminalAttemptId = serviceScalar(`
|
||
select result ->> 'attempt_id'
|
||
from (
|
||
select public.create_agentic_rectification_run_attempt(
|
||
'${userId}', '${terminalCaseId}', '${terminalTurnId}', 1
|
||
) as result
|
||
) claimed
|
||
`);
|
||
fixture.psql(`
|
||
update public.agentic_rectification_cases
|
||
set status = 'closed', completed_at = now(), closed_reason = 'other'
|
||
where id = '${terminalCaseId}';
|
||
`);
|
||
assert.equal(
|
||
serviceScalar(`
|
||
select (result ->> 'turn_id') || ':' ||
|
||
(result ->> 'should_execute') || ':' ||
|
||
(result ->> 'idempotent')
|
||
from (
|
||
select public.append_agentic_rectification_turn(
|
||
'${userId}', '${terminalCaseId}', '确认这个时间', null,
|
||
'test-model', null, 'pending', '${terminalRequestId}'
|
||
) as result
|
||
) replayed
|
||
`),
|
||
`${terminalTurnId}:false:true`,
|
||
);
|
||
assert.throws(
|
||
() => serviceScalar(`
|
||
select public.append_agentic_rectification_turn(
|
||
'${userId}', '${terminalCaseId}', '关闭后新请求', null,
|
||
'test-model', null, 'pending', 'dddddddd-dddd-4ddd-8ddd-dddddddddddd'
|
||
)
|
||
`),
|
||
/agentic_rectification_case_terminal/,
|
||
);
|
||
assert.equal(
|
||
serviceScalar(`
|
||
select (result ->> 'attempt_id') || ':' ||
|
||
(result ->> 'should_execute') || ':' ||
|
||
(result ->> 'already_in_progress') || ':' ||
|
||
(result ->> 'idempotent')
|
||
from (
|
||
select public.create_agentic_rectification_run_attempt(
|
||
'${userId}', '${terminalCaseId}', '${terminalTurnId}', 1
|
||
) as result
|
||
) replayed
|
||
`),
|
||
`${terminalAttemptId}:false:true:true`,
|
||
);
|
||
assert.throws(
|
||
() => serviceScalar(`
|
||
select public.create_agentic_rectification_run_attempt(
|
||
'${userId}', '${terminalCaseId}', '${terminalTurnId}', 2
|
||
)
|
||
`),
|
||
/agentic_rectification_case_terminal/,
|
||
);
|
||
serviceScalar(`
|
||
select public.insert_agentic_rectification_skill_run_receipt(
|
||
'${userId}', '${terminalCaseId}', '${terminalTurnId}', '${terminalAttemptId}',
|
||
'turn', 'jyotish-birth-time-rectification', '10.0.0',
|
||
'${skillSha256}', '${skillSourceCommit}'
|
||
)
|
||
`);
|
||
serviceScalar(`
|
||
select public.insert_agentic_rectification_run_phase(
|
||
'${userId}', '${terminalCaseId}', '${terminalTurnId}',
|
||
'billing.settled', null, 1, '${terminalAttemptId}'
|
||
)
|
||
`);
|
||
serviceScalar(`
|
||
select public.insert_agentic_rectification_run_phase(
|
||
'${userId}', '${terminalCaseId}', '${terminalTurnId}',
|
||
'run.completed', null, 2, '${terminalAttemptId}'
|
||
)
|
||
`);
|
||
assert.equal(
|
||
serviceScalar(`
|
||
select (result ->> 'status') || ':' || (result ->> 'idempotent')
|
||
from (
|
||
select public.finalize_agentic_rectification_run_attempt(
|
||
'${userId}', '${terminalCaseId}', '${terminalTurnId}', '${terminalAttemptId}',
|
||
'completed', null, '{"inputTokens":11,"outputTokens":7}'::jsonb
|
||
) as result
|
||
) finalized
|
||
`),
|
||
"completed:false",
|
||
);
|
||
assert.equal(
|
||
serviceScalar(`
|
||
select (result ->> 'status') || ':' || (result ->> 'idempotent')
|
||
from (
|
||
select public.finalize_agentic_rectification_turn(
|
||
'${userId}', '${terminalCaseId}', '${terminalTurnId}', '${terminalAttemptId}',
|
||
'completed', '已确认并完成', '${terminalAttemptId}'
|
||
) as result
|
||
) finalized
|
||
`),
|
||
"completed:false",
|
||
);
|
||
assert.throws(
|
||
() => serviceScalar(`
|
||
select public.finalize_agentic_rectification_turn(
|
||
'${userId}', '${terminalCaseId}', '${terminalTurnId}', '${terminalAttemptId}',
|
||
'failed', null, null
|
||
)
|
||
`),
|
||
/agentic_rectification_turn_already_completed/,
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select status || ':' || assistant_message || ':' || successful_attempt_id
|
||
from public.agentic_rectification_turns
|
||
where id = '${terminalTurnId}'
|
||
`),
|
||
`completed:已确认并完成:${terminalAttemptId}`,
|
||
);
|
||
|
||
const supersededCaseId = "88888888-8888-4888-8888-888888888888";
|
||
const supersededSessionId = "99999999-9999-4999-8999-999999999999";
|
||
const supersededRequestId = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
|
||
fixture.psql(`
|
||
insert into public.chat_sessions (
|
||
id, user_id, title, theme, model_id, messages, session_type, updated_at
|
||
) values (
|
||
'${supersededSessionId}', '${userId}', 'V10 superseded attempt', 'general',
|
||
'test-model', '[]', 'birth_time_rectification', now()
|
||
);
|
||
insert into public.agentic_rectification_cases (
|
||
id, user_id, session_id, status, skill_name, skill_version,
|
||
skill_sha256, skill_source_commit, baseline_profile_fingerprint,
|
||
baseline_birth_snapshot, candidate_range
|
||
) values (
|
||
'${supersededCaseId}', '${userId}', '${supersededSessionId}', 'collecting_evidence',
|
||
'jyotish-birth-time-rectification', '10.0.0', '${skillSha256}',
|
||
'${skillSourceCommit}', 'v10-superseded-fixture', '{}', '{}'
|
||
);
|
||
`);
|
||
const supersededTurnId = serviceScalar(`
|
||
select result ->> 'turn_id'
|
||
from (
|
||
select public.append_agentic_rectification_turn(
|
||
'${userId}', '${supersededCaseId}', '继续核对', null,
|
||
'test-model', null, 'pending', '${supersededRequestId}'
|
||
) as result
|
||
) appended
|
||
`);
|
||
const oldAttemptId = serviceScalar(`
|
||
select result ->> 'attempt_id'
|
||
from (
|
||
select public.create_agentic_rectification_run_attempt(
|
||
'${userId}', '${supersededCaseId}', '${supersededTurnId}', 1
|
||
) as result
|
||
) claimed
|
||
`);
|
||
serviceScalar(`
|
||
select public.finalize_agentic_rectification_run_attempt(
|
||
'${userId}', '${supersededCaseId}', '${supersededTurnId}', '${oldAttemptId}',
|
||
'retryable', 'stream_unfinished', '{}'::jsonb
|
||
)
|
||
`);
|
||
const latestAttemptId = serviceScalar(`
|
||
select result ->> 'attempt_id'
|
||
from (
|
||
select public.create_agentic_rectification_run_attempt(
|
||
'${userId}', '${supersededCaseId}', '${supersededTurnId}', 2
|
||
) as result
|
||
) claimed
|
||
`);
|
||
assert.notEqual(latestAttemptId, oldAttemptId);
|
||
assert.throws(
|
||
() => serviceScalar(`
|
||
select public.finalize_agentic_rectification_turn(
|
||
'${userId}', '${supersededCaseId}', '${supersededTurnId}', '${oldAttemptId}',
|
||
'retryable', null, null
|
||
)
|
||
`),
|
||
/agentic_rectification_attempt_superseded/,
|
||
);
|
||
|
||
const sessionId = "11111111-1111-4111-8111-111111111111";
|
||
const inserted = await local.from("chat_sessions").insert({
|
||
id: sessionId,
|
||
user_id: userId,
|
||
title: "Local conversation",
|
||
theme: "general",
|
||
model_id: "test-model",
|
||
messages: [],
|
||
session_type: "consultation",
|
||
rectification_case_id: null,
|
||
updated_at: new Date().toISOString(),
|
||
}).select("id").single();
|
||
assert.equal(inserted.error, null);
|
||
assert.deepEqual(inserted.data, { id: sessionId });
|
||
|
||
const pinDefaults = await local
|
||
.from("chat_sessions")
|
||
.select("pinned,archived_at")
|
||
.eq("id", sessionId)
|
||
.single();
|
||
assert.equal(pinDefaults.error, null);
|
||
assert.deepEqual(pinDefaults.data, { pinned: false, archived_at: null });
|
||
|
||
const ownerPin = await local
|
||
.from("chat_sessions")
|
||
.update({ pinned: true, archived_at: "2026-09-01T00:00:00.000Z" })
|
||
.eq("id", sessionId)
|
||
.select("pinned,archived_at")
|
||
.single();
|
||
assert.equal(ownerPin.error, null);
|
||
assert.equal((ownerPin.data as { pinned: boolean }).pinned, true);
|
||
assert.ok((ownerPin.data as { archived_at: string | null }).archived_at);
|
||
|
||
fixture.psqlAs(
|
||
"identity_runtime",
|
||
"identity-runtime-test-password",
|
||
`
|
||
insert into identity.users (name, email, email_verified, email_verified_at)
|
||
values ('Pin Archive Other', 'pin-archive-other@example.com', true, now())
|
||
`,
|
||
);
|
||
const otherUserId = fixture.psql(
|
||
"select id from identity.users where email = 'pin-archive-other@example.com'",
|
||
);
|
||
const other = createLocalPostgresDataClient(
|
||
fixture.connectionUrl("app_runtime", "app-runtime-test-password"),
|
||
{ id: otherUserId, email: "pin-archive-other@example.com" },
|
||
);
|
||
const stolen = await other
|
||
.from("chat_sessions")
|
||
.update({ pinned: false, archived_at: null })
|
||
.eq("id", sessionId)
|
||
.select("id");
|
||
assert.equal((Array.isArray(stolen.data) ? stolen.data : []).length, 0);
|
||
assert.equal(
|
||
fixture.psql(`select pinned from public.chat_sessions where id = '${sessionId}'`),
|
||
"t",
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`select archived_at is not null from public.chat_sessions where id = '${sessionId}'`),
|
||
"t",
|
||
);
|
||
|
||
const ownerRestore = await local
|
||
.from("chat_sessions")
|
||
.update({ pinned: false, archived_at: null })
|
||
.eq("id", sessionId)
|
||
.select("pinned,archived_at")
|
||
.single();
|
||
assert.equal(ownerRestore.error, null);
|
||
assert.deepEqual(ownerRestore.data, { pinned: false, archived_at: null });
|
||
|
||
const beforeTomorrow = await local
|
||
.from("chat_sessions")
|
||
.select("id")
|
||
.eq("user_id", userId)
|
||
.eq("session_type", "consultation")
|
||
.lte("updated_at", new Date(Date.now() + 86_400_000).toISOString())
|
||
.single();
|
||
assert.equal(beforeTomorrow.error, null);
|
||
assert.deepEqual(beforeTomorrow.data, { id: sessionId });
|
||
|
||
const matchingTitle = await local
|
||
.from("chat_sessions")
|
||
.select("id")
|
||
.eq("user_id", userId)
|
||
.like("title", "Local%")
|
||
.single();
|
||
assert.equal(matchingTitle.error, null);
|
||
assert.deepEqual(matchingTitle.data, { id: sessionId });
|
||
|
||
const injectedLike = await local
|
||
.from("chat_sessions")
|
||
.select("id")
|
||
.eq("user_id", userId)
|
||
.like("title", "Local%' OR true --");
|
||
assert.equal(injectedLike.error, null);
|
||
assert.deepEqual(injectedLike.data, []);
|
||
|
||
const rectificationSessionId = "22222222-2222-4222-8222-222222222222";
|
||
fixture.psql(`
|
||
update public.profiles
|
||
set birth_date = '1997-08-08',
|
||
reported_birth_time = '05:00',
|
||
birth_time_source = 'family_exact',
|
||
uncertainty_before_minutes = 10,
|
||
uncertainty_after_minutes = 10,
|
||
latitude = 36.420487,
|
||
longitude = 114.209936,
|
||
timezone_offset = 8,
|
||
birth_time_status = 'reported'
|
||
where id = '${userId}';
|
||
insert into public.chat_sessions (id, user_id, title, theme, model_id, messages, session_type, updated_at)
|
||
values ('${rectificationSessionId}', '${userId}', 'Rectification', 'general', 'test-model', '[]', 'birth_time_rectification', now());
|
||
insert into public.agentic_rectification_results (
|
||
id, user_id, session_id, engine_result_id, canonical_input_hash, algorithm_version,
|
||
candidate_range, candidates, overall_confidence, selection_allowed, confirmation_allowed,
|
||
representative_time, baseline_birth_date, baseline_reported_birth_time, baseline_birth_time_source,
|
||
baseline_uncertainty_before_minutes, baseline_uncertainty_after_minutes, baseline_latitude,
|
||
baseline_longitude, baseline_timezone_offset
|
||
) values (
|
||
'33333333-3333-4333-8333-333333333333', '${userId}', '${rectificationSessionId}',
|
||
'engine-result-1', 'canonical-hash-1', 'test-v1', '{}',
|
||
'[{"rank":1,"time":"04:55","relative_support":60,"tied_minute_count":1},{"rank":2,"time":"05:07","relative_support":40,"tied_minute_count":1}]',
|
||
'medium', true, false, '04:55', '1997-08-08', '05:00', 'family_exact', 10, 10,
|
||
36.420487, 114.209936, 8
|
||
);
|
||
`);
|
||
const latestCandidate = await loadLatestAgenticRectificationResult(
|
||
admin as never,
|
||
userId,
|
||
rectificationSessionId,
|
||
);
|
||
assert.equal(latestCandidate?.resultId, "33333333-3333-4333-8333-333333333333");
|
||
assert.equal(latestCandidate?.selectionAllowed, true);
|
||
assert.deepEqual(latestCandidate?.candidates.map(({ time, relative_support }) => ({ time, relative_support })), [
|
||
{ time: "04:55", relative_support: 60 },
|
||
{ time: "05:07", relative_support: 40 },
|
||
]);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select
|
||
not has_function_privilege('service_role', 'public.accept_agentic_rectification_candidate(uuid,uuid,uuid,time without time zone)', 'EXECUTE')
|
||
and has_function_privilege('service_role', 'public.accept_agentic_rectification_candidate_for_case_v2(uuid,uuid,uuid,uuid,uuid,jsonb)', 'EXECUTE')
|
||
`),
|
||
"t",
|
||
);
|
||
assert.throws(
|
||
() => fixture.psqlAs(
|
||
"service_runtime",
|
||
"service-runtime-test-password",
|
||
`set role service_role;
|
||
select public.accept_agentic_rectification_candidate(
|
||
'${userId}', '${rectificationSessionId}', '33333333-3333-4333-8333-333333333333', '04:55'
|
||
)`,
|
||
),
|
||
/permission denied for function accept_agentic_rectification_candidate/,
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`select coalesce(to_char(active_birth_time, 'HH24:MI'), 'null') || ':' || birth_time_status || ':' || to_char(reported_birth_time, 'HH24:MI') from public.profiles where id = '${userId}'`),
|
||
"null:reported:05:00",
|
||
);
|
||
|
||
const rectificationCaseId = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
||
const baselineBirthSnapshot = {
|
||
birth_date: "1997-08-08",
|
||
reported_birth_time: "05:00",
|
||
active_birth_time: null,
|
||
birth_time_source: "family_exact",
|
||
birth_time_period: null,
|
||
uncertainty_before_minutes: 10,
|
||
uncertainty_after_minutes: 10,
|
||
latitude: 36.420487,
|
||
longitude: 114.209936,
|
||
timezone_offset: 8,
|
||
};
|
||
const candidateRange = { start_time: "04:50", end_time: "05:10" };
|
||
const candidateInferenceState = {
|
||
algorithm_version: "database-local-business-v2",
|
||
candidate_set_id: "04:50-05:10:04:55,05:07",
|
||
revision: 0,
|
||
phase: "candidate_ready",
|
||
result_status: "completed_with_range",
|
||
range_start: "04:50",
|
||
range_end: "05:10",
|
||
candidates: [
|
||
{ id: "04:55", time: "04:55", cluster_range: ["04:55", "04:55"], prior_score: 60, posterior_score: 60, probability: 0.6, status: "active", rank: 1, strong_conflict_count: 0 },
|
||
{ id: "05:07", time: "05:07", cluster_range: ["05:07", "05:07"], prior_score: 40, posterior_score: 40, probability: 0.4, status: "active", rank: 2, strong_conflict_count: 0 },
|
||
],
|
||
events: [],
|
||
probes: [],
|
||
answered_probes: [],
|
||
rounds: [],
|
||
entropy: 1,
|
||
representative_time: "04:55",
|
||
credible_range: ["04:55", "05:07"],
|
||
};
|
||
fixture.psql(`
|
||
insert into public.agentic_rectification_cases (
|
||
id, user_id, session_id, status, skill_name, skill_version,
|
||
baseline_profile_fingerprint, baseline_birth_snapshot, candidate_range
|
||
) values (
|
||
'${rectificationCaseId}', '${userId}', '${rectificationSessionId}', 'candidate_ready',
|
||
'jyotish-birth-time-rectification', '10.0.0', '${"e".repeat(64)}',
|
||
'${JSON.stringify(baselineBirthSnapshot)}'::jsonb,
|
||
'${JSON.stringify(candidateRange)}'::jsonb
|
||
)
|
||
`);
|
||
|
||
const persisted = await admin.rpc("persist_agentic_rectification_candidate_v2", {
|
||
p_user_id: userId,
|
||
p_case_id: rectificationCaseId,
|
||
p_engine_result_id: "database-local-business-v2",
|
||
p_evidence_ledger_fingerprint: "f".repeat(64),
|
||
p_candidate_range_fingerprint: "1".repeat(64),
|
||
p_skill_version: "10.0.0",
|
||
p_algorithm_version: "database-local-business-v2",
|
||
p_event_contract_version: "rectification-event-contract-v2",
|
||
p_decision_policy_version: "decision-policy-v2",
|
||
p_candidate_range: candidateRange,
|
||
p_candidates: [
|
||
{ rank: 1, time: "04:55", relative_support: 60, tied_minute_count: 1 },
|
||
{ rank: 2, time: "05:07", relative_support: 40, tied_minute_count: 1 },
|
||
],
|
||
p_decision_receipt: {
|
||
display_allowed: true,
|
||
accept_allowed: true,
|
||
confirm_allowed: false,
|
||
representative_time: "04:55",
|
||
overall_confidence: "medium",
|
||
margin_percent: 20,
|
||
inference_state: candidateInferenceState,
|
||
},
|
||
p_execution_ledger: [
|
||
{ phase: "candidate.score", status: "completed", engine: "fixture-engine" },
|
||
{ phase: "decision.evaluate", status: "completed", policy: "decision-policy-v2" },
|
||
],
|
||
});
|
||
assert.equal(persisted.error, null, rpcError(persisted.error));
|
||
const persistedRow = persisted.data as Record<string, unknown>;
|
||
const v2ResultId = String(persistedRow.result_id);
|
||
const [firstCandidateId, secondCandidateId] = candidateIds(persistedRow);
|
||
assert.ok(firstCandidateId);
|
||
assert.ok(secondCandidateId);
|
||
|
||
const firstAcceptRequestId = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
|
||
const firstAccepted = await admin.rpc("accept_agentic_rectification_candidate_for_case_v2", {
|
||
p_user_id: userId,
|
||
p_case_id: rectificationCaseId,
|
||
p_result_id: v2ResultId,
|
||
p_candidate_id: firstCandidateId,
|
||
p_request_id: firstAcceptRequestId,
|
||
});
|
||
assert.equal(firstAccepted.error, null, rpcError(firstAccepted.error));
|
||
assert.deepEqual(
|
||
{
|
||
saved_time: (firstAccepted.data as Record<string, unknown>).saved_time,
|
||
status: (firstAccepted.data as Record<string, unknown>).status,
|
||
case_status: (firstAccepted.data as Record<string, unknown>).case_status,
|
||
idempotent: (firstAccepted.data as Record<string, unknown>).idempotent,
|
||
},
|
||
{ saved_time: "04:55", status: "accepted", case_status: "candidate_accepted", idempotent: false },
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select to_char(active_birth_time, 'HH24:MI') || ':' ||
|
||
to_char(birth_time, 'HH24:MI') || ':' || birth_time_status || ':' ||
|
||
to_char(reported_birth_time, 'HH24:MI')
|
||
from public.profiles where id = '${userId}'
|
||
`),
|
||
"04:55:04:55:accepted:05:00",
|
||
);
|
||
|
||
const firstReplay = await admin.rpc("accept_agentic_rectification_candidate_for_case_v2", {
|
||
p_user_id: userId,
|
||
p_case_id: rectificationCaseId,
|
||
p_result_id: v2ResultId,
|
||
p_candidate_id: firstCandidateId,
|
||
p_request_id: firstAcceptRequestId,
|
||
});
|
||
assert.equal(firstReplay.error, null, rpcError(firstReplay.error));
|
||
assert.equal((firstReplay.data as Record<string, unknown>).idempotent, true);
|
||
|
||
const secondAccepted = await admin.rpc("accept_agentic_rectification_candidate_for_case_v2", {
|
||
p_user_id: userId,
|
||
p_case_id: rectificationCaseId,
|
||
p_result_id: v2ResultId,
|
||
p_candidate_id: secondCandidateId,
|
||
p_request_id: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
|
||
});
|
||
assert.equal(secondAccepted.error, null, rpcError(secondAccepted.error));
|
||
assert.deepEqual(
|
||
{
|
||
saved_time: (secondAccepted.data as Record<string, unknown>).saved_time,
|
||
status: (secondAccepted.data as Record<string, unknown>).status,
|
||
case_status: (secondAccepted.data as Record<string, unknown>).case_status,
|
||
idempotent: (secondAccepted.data as Record<string, unknown>).idempotent,
|
||
},
|
||
{ saved_time: "05:07", status: "accepted", case_status: "candidate_accepted", idempotent: false },
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`select to_char(active_birth_time, 'HH24:MI') || ':' || to_char(birth_time, 'HH24:MI') || ':' || to_char(reported_birth_time, 'HH24:MI') from public.profiles where id = '${userId}'`),
|
||
"05:07:05:07:05:00",
|
||
);
|
||
|
||
assert.equal(
|
||
fixture.psql(`select invalidated_at is null from public.agentic_rectification_results where id = '33333333-3333-4333-8333-333333333333'`),
|
||
"t",
|
||
);
|
||
fixture.psql(`update public.profiles set reported_birth_time = '05:01' where id = '${userId}'`);
|
||
assert.equal(
|
||
fixture.psql(`select invalidated_at is not null from public.agentic_rectification_results where id = '33333333-3333-4333-8333-333333333333'`),
|
||
"t",
|
||
);
|
||
assert.equal(
|
||
fixture.psql(`select invalidated_at is not null from public.agentic_rectification_results where id = '${v2ResultId}'`),
|
||
"t",
|
||
);
|
||
const expiredAccept = await admin.rpc("accept_agentic_rectification_candidate_for_case_v2", {
|
||
p_user_id: userId,
|
||
p_case_id: rectificationCaseId,
|
||
p_result_id: v2ResultId,
|
||
p_candidate_id: firstCandidateId,
|
||
p_request_id: "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
||
});
|
||
assert.match(rpcError(expiredAccept.error), /agentic_rectification_candidate_expired/);
|
||
assert.equal(
|
||
await loadLatestAgenticRectificationResult(admin as never, userId, rectificationSessionId),
|
||
null,
|
||
);
|
||
|
||
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",
|
||
);
|
||
|
||
const consultationSessionId = "88888888-8888-4888-8888-888888888888";
|
||
fixture.psql(`
|
||
insert into public.chat_sessions (
|
||
id, user_id, title, theme, model_id, messages, session_type, updated_at
|
||
) values (
|
||
'${consultationSessionId}', '${userId}', '新对话', 'general',
|
||
'test-model', '[]', 'consultation', now()
|
||
);
|
||
`);
|
||
const firstQuestion = await admin.rpc("append_consultation_question", {
|
||
p_user_id: userId,
|
||
p_request_id: "append-request-1",
|
||
p_session_id: consultationSessionId,
|
||
p_question_message: { role: "user", text: "第一问会不会丢" },
|
||
});
|
||
assert.equal(firstQuestion.error, null, rpcError(firstQuestion.error));
|
||
const firstQuestionRow = Array.isArray(firstQuestion.data) ? firstQuestion.data[0] : firstQuestion.data;
|
||
assert.equal((firstQuestionRow as { success?: unknown }).success, true);
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select title || ':' || jsonb_array_length(messages)::text || ':' || (messages->0->>'text')
|
||
from public.chat_sessions where id = '${consultationSessionId}'
|
||
`),
|
||
"第一问会不会丢:1:第一问会不会丢",
|
||
);
|
||
|
||
const firstQuestionReplay = await admin.rpc("append_consultation_question", {
|
||
p_user_id: userId,
|
||
p_request_id: "append-request-1",
|
||
p_session_id: consultationSessionId,
|
||
p_question_message: { role: "user", text: "不该写入的重复提问" },
|
||
});
|
||
assert.equal(firstQuestionReplay.error, null, rpcError(firstQuestionReplay.error));
|
||
const firstQuestionReplayRow = Array.isArray(firstQuestionReplay.data) ? firstQuestionReplay.data[0] : firstQuestionReplay.data;
|
||
assert.equal((firstQuestionReplayRow as { success?: unknown }).success, true);
|
||
assert.equal(
|
||
fixture.psql(`select jsonb_array_length(messages) from public.chat_sessions where id = '${consultationSessionId}'`),
|
||
"1",
|
||
);
|
||
|
||
const secondQuestion = await admin.rpc("append_consultation_question", {
|
||
p_user_id: userId,
|
||
p_request_id: "append-request-2",
|
||
p_session_id: consultationSessionId,
|
||
p_question_message: { role: "user", text: "第二问也会留下" },
|
||
});
|
||
assert.equal(secondQuestion.error, null, rpcError(secondQuestion.error));
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select jsonb_array_length(messages)::text || ':' || (messages->0->>'text') || ':' || (messages->1->>'text')
|
||
from public.chat_sessions where id = '${consultationSessionId}'
|
||
`),
|
||
"2:第一问会不会丢:第二问也会留下",
|
||
);
|
||
|
||
fixture.psql(`
|
||
update public.chat_sessions
|
||
set messages = (
|
||
select coalesce(jsonb_agg(jsonb_build_object('role','user','text','x','requestId', n::text)), '[]'::jsonb)
|
||
from generate_series(1, 200) as n
|
||
)
|
||
where id = '${consultationSessionId}'
|
||
`);
|
||
const sessionFull = await admin.rpc("append_consultation_question", {
|
||
p_user_id: userId,
|
||
p_request_id: "append-request-full",
|
||
p_session_id: consultationSessionId,
|
||
p_question_message: { role: "user", text: "满了就不能再写" },
|
||
});
|
||
assert.equal(sessionFull.error, null, rpcError(sessionFull.error));
|
||
const sessionFullRow = Array.isArray(sessionFull.data) ? sessionFull.data[0] : sessionFull.data;
|
||
assert.deepEqual(sessionFullRow, { success: false, error_code: "session_full" });
|
||
assert.equal(
|
||
fixture.psql(`
|
||
select jsonb_array_length(messages)
|
||
from public.chat_sessions where id = '${consultationSessionId}'
|
||
`),
|
||
"200",
|
||
);
|
||
|
||
fixture.psql(`
|
||
insert into public.redemption_codes (code_hash, code_mask, credits)
|
||
values ('${"a".repeat(64)}', 'JYOTISH-****-TEST', 3)
|
||
`);
|
||
const redeemed = await local.rpc("redeem_code", {
|
||
p_code_hash: "a".repeat(64),
|
||
});
|
||
assert.equal(redeemed.error, null);
|
||
assert.deepEqual(redeemed.data, [{ success: true, credits: 3, awarded_credits: 3, error_code: null }]);
|
||
} finally {
|
||
await closeLocalPostgresDataPools();
|
||
fixture.stop();
|
||
}
|
||
});
|