Files
Jyotisha/frontend/tests/rectification-v9-database.test.ts
Jesse_Chen 2984b6d1d0
Independent Staging Quality Gate / validate (push) Successful in 12m25s
Independent Staging Quality Gate / publish (push) Successful in 8m48s
test(rectification): align staging gate contracts
2026-08-15 18:17:51 +08:00

1276 lines
54 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 { closeLocalPostgresDataPool, createLocalPostgresDataClient } from "../src/lib/db/local-postgres-client-core.ts";
import { startPostgresFixture } from "./helpers/postgres-fixture.ts";
const runnerPath = fileURLToPath(
new URL("../scripts/db-migrate.mjs", import.meta.url),
);
function dockerAvailable(): boolean {
const probe = spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], {
encoding: "utf8",
stdio: "ignore",
});
return probe.status === 0;
}
const skipWithoutDocker = dockerAvailable() ? false : "docker unavailable on this host";
const snapshot = {
birth_date: "1997-08-08",
latitude: 36.420487,
longitude: 114.209936,
timezone_offset: 8,
birth_time_source: "family_exact",
reported_birth_time: "05:00",
active_birth_time: null,
birth_time_period: null,
uncertainty_before_minutes: 10,
uncertainty_after_minutes: 10,
};
const fingerprint = "a".repeat(64);
const range = { start_time: "04:50", end_time: "05:10" };
const skillName = "jyotish-birth-time-rectification";
const skillVersion = "9.0.0";
function rpcError(error: unknown): string {
if (!error || typeof error !== "object") return "";
const value = error as { message?: unknown };
return typeof value.message === "string" ? value.message : "";
}
test("v9 migration applies on a fresh database and re-applies idempotently", { skip: skipWithoutDocker }, async () => {
const fixture = startPostgresFixture();
const schemaUrl = fixture.connectionUrl("schema_owner", "schema-owner-test-password");
const migrate = () =>
spawnSync(process.execPath, [runnerPath], {
encoding: "utf8",
env: { ...process.env, SCHEMA_DATABASE_URL: schemaUrl },
});
try {
const firstRun = migrate();
assert.equal(firstRun.status, 0, firstRun.stderr);
assert.match(firstRun.stdout, /applied 20260812010000_agentic_rectification_v9_runtime\.sql/);
const secondRun = migrate();
assert.equal(secondRun.status, 0, secondRun.stderr);
assert.match(
secondRun.stdout,
/already applied 20260812010000_agentic_rectification_v9_runtime\.sql/,
);
} finally {
fixture.stop();
}
});
test("v9 open is atomic, idempotent and allows separate homepage cases", { skip: skipWithoutDocker }, 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);
fixture.psqlAs(
"identity_runtime",
"identity-runtime-test-password",
`
insert into identity.users (name, email, email_verified, email_verified_at)
values ('V9 User', 'v9-user@example.com', true, now())
`,
);
const userId = fixture.psql(
"select id from identity.users where email = 'v9-user@example.com'",
);
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}';
`);
fixture.psql(`
with provider as (
insert into public.model_providers (code, name, provider_type, encrypted_api_key, enabled)
values ('v9-test', 'V9 Test', 'openai', 'test-ciphertext', true)
returning id
), config as (
insert into public.model_configs (model_id)
values ('v9-default-model')
returning id
)
insert into public.model_config_versions (
config_id, version, provider_id, label, provider_model, enabled, is_default, status, published_at
)
select config.id, 1, provider.id, 'V9 Default', 'gpt-test', true, true, 'published', now()
from config cross join provider;
`);
const service = createLocalPostgresDataClient(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
null,
"service_role",
);
const first = await service.rpc("open_agentic_rectification_case", {
p_user_id: userId,
p_request_id: "11111111-1111-4111-8111-111111111111",
p_intent: "homepage",
p_session_id: null,
p_skill_name: skillName,
p_skill_version: skillVersion,
p_baseline_profile_fingerprint: fingerprint,
p_baseline_birth_snapshot: snapshot,
p_candidate_range: range,
});
assert.equal(first.error, null, rpcError(first.error));
const created = first.data as Record<string, unknown>;
assert.equal(created.disposition, "created");
assert.equal(created.should_start_opening, true);
assert.equal(created.status, "draft");
const caseId = String(created.case_id);
const sessionId = String(created.session_id);
assert.equal(
fixture.psql(`select count(*) from public.agentic_rectification_cases where user_id = '${userId}'`),
"1",
);
assert.equal(
fixture.psql(`select count(*) from public.chat_sessions where user_id = '${userId}'`),
"1",
);
assert.equal(
fixture.psql(`select model_id || ':' || model_config_version from public.chat_sessions where id = '${sessionId}'`),
"v9-default-model:1",
);
assert.equal(
fixture.psql(
`select count(*) from public.agentic_rectification_open_ledger where user_id = '${userId}'`,
),
"1",
);
assert.equal(
fixture.psql(
`select agentic_rectification_case_id from public.chat_sessions where id = '${sessionId}'`,
),
caseId,
);
// Replaying the same requestId must not create anything new.
const replay = await service.rpc("open_agentic_rectification_case", {
p_user_id: userId,
p_request_id: "11111111-1111-4111-8111-111111111111",
p_intent: "homepage",
p_session_id: null,
p_skill_name: skillName,
p_skill_version: skillVersion,
p_baseline_profile_fingerprint: fingerprint,
p_baseline_birth_snapshot: snapshot,
p_candidate_range: range,
});
assert.equal(replay.error, null, rpcError(replay.error));
assert.equal((replay.data as Record<string, unknown>).case_id, caseId);
assert.equal((replay.data as Record<string, unknown>).session_id, sessionId);
assert.equal(
fixture.psql(`select count(*) from public.chat_sessions where user_id = '${userId}'`),
"1",
);
// A fresh homepage click creates a separate case and session while the
// previous unfinished case remains available through exact-session history.
const secondOpen = await service.rpc("open_agentic_rectification_case", {
p_user_id: userId,
p_request_id: "22222222-2222-4222-8222-222222222222",
p_intent: "homepage",
p_session_id: null,
p_skill_name: skillName,
p_skill_version: skillVersion,
p_baseline_profile_fingerprint: fingerprint,
p_baseline_birth_snapshot: snapshot,
p_candidate_range: range,
});
assert.equal(secondOpen.error, null, rpcError(secondOpen.error));
assert.equal((secondOpen.data as Record<string, unknown>).disposition, "created");
assert.equal((secondOpen.data as Record<string, unknown>).should_start_opening, true);
const secondCaseId = String((secondOpen.data as Record<string, unknown>).case_id);
const secondSessionId = String((secondOpen.data as Record<string, unknown>).session_id);
assert.notEqual(secondCaseId, caseId);
assert.notEqual(secondSessionId, sessionId);
assert.equal(
fixture.psql(`select count(*) from public.agentic_rectification_cases where user_id = '${userId}' and status = any (public.agentic_rectification_resumable_statuses())`),
"2",
);
assert.equal(
fixture.psql(`select count(*) from public.chat_sessions where user_id = '${userId}'`),
"2",
);
// The explicit new intent has the same create semantics.
const thirdOpen = await service.rpc("open_agentic_rectification_case", {
p_user_id: userId,
p_request_id: "33333333-3333-4333-8333-333333333333",
p_intent: "new",
p_session_id: null,
p_skill_name: skillName,
p_skill_version: skillVersion,
p_baseline_profile_fingerprint: fingerprint,
p_baseline_birth_snapshot: snapshot,
p_candidate_range: range,
});
assert.equal(thirdOpen.error, null, rpcError(thirdOpen.error));
assert.equal((thirdOpen.data as Record<string, unknown>).disposition, "created");
assert.equal(
fixture.psql(`select count(*) from public.agentic_rectification_cases where user_id = '${userId}' and status = any (public.agentic_rectification_resumable_statuses())`),
"3",
);
// Session intent opens the exact session.
const sessionOpen = await service.rpc("open_agentic_rectification_case", {
p_user_id: userId,
p_request_id: "44444444-4444-4444-8444-444444444444",
p_intent: "session",
p_session_id: sessionId,
p_skill_name: skillName,
p_skill_version: skillVersion,
p_baseline_profile_fingerprint: "session-view",
p_baseline_birth_snapshot: {},
p_candidate_range: { start_time: "00:00", end_time: "23:59" },
});
assert.equal(sessionOpen.error, null, rpcError(sessionOpen.error));
assert.equal((sessionOpen.data as Record<string, unknown>).case_id, caseId);
assert.equal((sessionOpen.data as Record<string, unknown>).session_id, sessionId);
} finally {
// The service client registered a per-URL pool in the global cache; close
// only this test's own service URL before destroying PostgreSQL, otherwise
// the async pool teardown surfaces 57P01 after the fixture is gone. The
// fixture must still stop even if pool close fails.
try {
await closeLocalPostgresDataPool(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
);
} finally {
fixture.stop();
}
}
});
test("v9 enforces profile gating, ownership and terminal read-only", { skip: skipWithoutDocker }, 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);
fixture.psqlAs(
"identity_runtime",
"identity-runtime-test-password",
`
insert into identity.users (name, email, email_verified, email_verified_at) values
('Incomplete', 'incomplete@example.com', true, now()),
('Owner', 'owner@example.com', true, now());
`,
);
const incompleteId = fixture.psql(
"select id from identity.users where email = 'incomplete@example.com'",
);
const ownerId = fixture.psql(
"select id from identity.users where email = 'owner@example.com'",
);
fixture.psql(`
update public.profiles set birth_date = '1997-08-08' where id = '${incompleteId}';
update public.profiles
set birth_date = '1997-08-08',
reported_birth_time = '05:00',
birth_time_source = 'family_exact',
latitude = 36.420487, longitude = 114.209936, timezone_offset = 8,
birth_time_status = 'reported'
where id = '${ownerId}';
`);
const service = createLocalPostgresDataClient(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
null,
"service_role",
);
// Incomplete profile: no case is created.
const incomplete = await service.rpc("open_agentic_rectification_case", {
p_user_id: incompleteId,
p_request_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
p_intent: "homepage",
p_session_id: null,
p_skill_name: skillName,
p_skill_version: skillVersion,
p_baseline_profile_fingerprint: fingerprint,
p_baseline_birth_snapshot: { birth_date: "1997-08-08" },
p_candidate_range: range,
});
assert.match(rpcError(incomplete.error), /agentic_rectification_profile_incomplete/);
assert.equal(
fixture.psql(`select count(*) from public.agentic_rectification_cases where user_id = '${incompleteId}'`),
"0",
);
// Owner opens a case, closes it, then reopens it read-only.
const opened = await service.rpc("open_agentic_rectification_case", {
p_user_id: ownerId,
p_request_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
p_intent: "homepage",
p_session_id: null,
p_skill_name: skillName,
p_skill_version: skillVersion,
p_baseline_profile_fingerprint: fingerprint,
p_baseline_birth_snapshot: snapshot,
p_candidate_range: range,
});
assert.equal(opened.error, null, rpcError(opened.error));
const ownerCaseId = String((opened.data as Record<string, unknown>).case_id);
const ownerSessionId = String((opened.data as Record<string, unknown>).session_id);
const closed = await service.rpc("close_agentic_rectification_case", {
p_user_id: ownerId,
p_case_id: ownerCaseId,
p_reason: "completed_by_user",
});
assert.equal(closed.error, null, rpcError(closed.error));
assert.equal((closed.data as Record<string, unknown>).status, "closed");
assert.equal(
fixture.psql(`select status from public.agentic_rectification_cases where id = '${ownerCaseId}'`),
"closed",
);
assert.equal(
fixture.psql(`select completed_at is not null from public.agentic_rectification_cases where id = '${ownerCaseId}'`),
"t",
);
const readonly = await service.rpc("open_agentic_rectification_case", {
p_user_id: ownerId,
p_request_id: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
p_intent: "session",
p_session_id: ownerSessionId,
p_skill_name: skillName,
p_skill_version: skillVersion,
p_baseline_profile_fingerprint: "session-view",
p_baseline_birth_snapshot: {},
p_candidate_range: { start_time: "00:00", end_time: "23:59" },
});
assert.equal(readonly.error, null, rpcError(readonly.error));
assert.equal((readonly.data as Record<string, unknown>).disposition, "readonly");
assert.equal((readonly.data as Record<string, unknown>).should_start_opening, false);
// A different user cannot open the owner's session.
const foreign = await service.rpc("open_agentic_rectification_case", {
p_user_id: incompleteId,
p_request_id: "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
p_intent: "session",
p_session_id: ownerSessionId,
p_skill_name: skillName,
p_skill_version: skillVersion,
p_baseline_profile_fingerprint: "session-view",
p_baseline_birth_snapshot: {},
p_candidate_range: { start_time: "00:00", end_time: "23:59" },
});
assert.match(rpcError(foreign.error), /agentic_rectification_session_not_found/);
// Terminal cases reject evidence proposals and turn appends.
const turn = await service.rpc("append_agentic_rectification_turn", {
p_user_id: ownerId,
p_case_id: ownerCaseId,
p_user_message: "我2016年9月离开家去北京工作",
p_assistant_message: "已记录这段经历。",
p_model_name: "test-model",
p_model_version: "1",
p_status: "completed",
p_request_id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
});
assert.match(rpcError(turn.error), /agentic_rectification_case_terminal/);
const proposal = await service.rpc("propose_agentic_rectification_evidence", {
p_user_id: ownerId,
p_case_id: ownerCaseId,
p_source_turn_id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
p_user_quote: "离开家去北京",
p_subject: "self",
p_event_kind: "relocation",
p_domain: "relocation",
p_occurred_from: "2016-09-01",
p_occurred_to: null,
p_date_precision: "month",
p_summary: "2016年9月离开家去北京",
});
assert.match(rpcError(proposal.error), /agentic_rectification_case_terminal/);
} finally {
try {
await closeLocalPostgresDataPool(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
);
} finally {
fixture.stop();
}
}
});
test("v9 evidence lifecycle: quote grounding, idempotency, confirm and revision lineage", { skip: skipWithoutDocker }, 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);
fixture.psqlAs(
"identity_runtime",
"identity-runtime-test-password",
`
insert into identity.users (name, email, email_verified, email_verified_at)
values ('Evidence', 'evidence@example.com', true, now())
`,
);
const userId = fixture.psql(
"select id from identity.users where email = 'evidence@example.com'",
);
fixture.psql(`
update public.profiles
set birth_date = '1997-08-08',
reported_birth_time = '05:00',
birth_time_source = 'family_exact',
latitude = 36.420487, longitude = 114.209936, timezone_offset = 8,
birth_time_status = 'reported'
where id = '${userId}';
`);
const service = createLocalPostgresDataClient(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
null,
"service_role",
);
const opened = await service.rpc("open_agentic_rectification_case", {
p_user_id: userId,
p_request_id: "ffffffff-ffff-4fff-8fff-ffffffffffff",
p_intent: "homepage",
p_session_id: null,
p_skill_name: skillName,
p_skill_version: skillVersion,
p_baseline_profile_fingerprint: fingerprint,
p_baseline_birth_snapshot: snapshot,
p_candidate_range: range,
});
assert.equal(opened.error, null, rpcError(opened.error));
const caseId = String((opened.data as Record<string, unknown>).case_id);
const turn = await service.rpc("append_agentic_rectification_turn", {
p_user_id: userId,
p_case_id: caseId,
p_user_message: "2016年9月我离开家去北京开始工作",
p_assistant_message: "好的,已记录。",
p_model_name: "test-model",
p_model_version: "1",
p_status: "completed",
p_request_id: "12121212-1212-4212-8212-121212121212",
});
assert.equal(turn.error, null, rpcError(turn.error));
const turnId = String((turn.data as Record<string, unknown>).turn_id);
const proposed = await service.rpc("propose_agentic_rectification_evidence", {
p_user_id: userId,
p_case_id: caseId,
p_source_turn_id: turnId,
p_user_quote: "离开家去北京",
p_subject: "self",
p_event_kind: "relocation",
p_domain: "relocation",
p_occurred_from: "2016-09-01",
p_occurred_to: null,
p_date_precision: "month",
p_summary: "2016年9月离开家去北京",
});
assert.equal(proposed.error, null, rpcError(proposed.error));
const evidenceId = String((proposed.data as Record<string, unknown>).evidence_id);
assert.equal((proposed.data as Record<string, unknown>).idempotent, false);
assert.equal(
fixture.psql(`select status from public.agentic_rectification_evidence where id = '${evidenceId}'`),
"draft",
);
// Replay must not create a second evidence row.
const replay = await service.rpc("propose_agentic_rectification_evidence", {
p_user_id: userId,
p_case_id: caseId,
p_source_turn_id: turnId,
p_user_quote: "离开家去北京",
p_subject: "self",
p_event_kind: "relocation",
p_domain: "relocation",
p_occurred_from: "2016-09-01",
p_occurred_to: null,
p_date_precision: "month",
p_summary: "2016年9月离开家去北京",
});
assert.equal(replay.error, null, rpcError(replay.error));
assert.equal((replay.data as Record<string, unknown>).evidence_id, evidenceId);
assert.equal((replay.data as Record<string, unknown>).idempotent, true);
assert.equal(
fixture.psql(`select count(*) from public.agentic_rectification_evidence where case_id = '${caseId}'`),
"1",
);
// Quotes not present in the source turn are rejected.
const ungrounded = await service.rpc("propose_agentic_rectification_evidence", {
p_user_id: userId,
p_case_id: caseId,
p_source_turn_id: turnId,
p_user_quote: "去了上海",
p_subject: "self",
p_event_kind: "relocation",
p_domain: "relocation",
p_occurred_from: "2020-01-01",
p_occurred_to: null,
p_date_precision: "year",
p_summary: "2020年去了上海",
});
assert.match(rpcError(ungrounded.error), /agentic_rectification_quote_not_grounded/);
// Confirm transitions draft -> confirmed with a timestamp.
const confirmed = await service.rpc("confirm_agentic_rectification_evidence", {
p_user_id: userId,
p_case_id: caseId,
p_evidence_id: evidenceId,
});
assert.equal(confirmed.error, null, rpcError(confirmed.error));
assert.equal((confirmed.data as Record<string, unknown>).status, "confirmed");
assert.equal(
fixture.psql(`select confirmed_at is not null from public.agentic_rectification_evidence where id = '${evidenceId}'`),
"t",
);
// Revision creates a superseding row and never overwrites history.
const revised = await service.rpc("revise_agentic_rectification_evidence", {
p_user_id: userId,
p_case_id: caseId,
p_evidence_id: evidenceId,
p_user_quote: "离开家去北京",
p_occurred_from: "2016-10-01",
p_occurred_to: null,
p_date_precision: "month",
p_summary: "2016年10月离开家去北京",
});
assert.equal(revised.error, null, rpcError(revised.error));
const revisionId = String((revised.data as Record<string, unknown>).evidence_id);
assert.equal((revised.data as Record<string, unknown>).supersedes_evidence_id, evidenceId);
assert.equal(
fixture.psql(`select status from public.agentic_rectification_evidence where id = '${evidenceId}'`),
"superseded",
);
assert.equal(
fixture.psql(`select status from public.agentic_rectification_evidence where id = '${revisionId}'`),
"pending_confirmation",
);
assert.equal(
fixture.psql(
`select count(*) from public.agentic_rectification_evidence where case_id = '${caseId}'`,
),
"2",
);
} finally {
try {
await closeLocalPostgresDataPool(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
);
} finally {
fixture.stop();
}
}
});
test("v9 legacy backfill maps statuses, keeps one resumable per user and is idempotent", { skip: skipWithoutDocker }, 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);
fixture.psqlAs(
"identity_runtime",
"identity-runtime-test-password",
`
insert into identity.users (name, email, email_verified, email_verified_at) values
('Legacy', 'legacy@example.com', true, now()),
('LegacyConfirmed', 'legacy-confirmed@example.com', true, now()),
('LegacyCollect', 'legacy-collect@example.com', true, now()),
('LegacyDraft', 'legacy-draft@example.com', true, now()),
('LegacyDup', 'legacy-dup@example.com', true, now());
`,
);
const legacyId = fixture.psql("select id from identity.users where email = 'legacy@example.com'");
const confirmedId = fixture.psql("select id from identity.users where email = 'legacy-confirmed@example.com'");
const collectId = fixture.psql("select id from identity.users where email = 'legacy-collect@example.com'");
const draftId = fixture.psql("select id from identity.users where email = 'legacy-draft@example.com'");
const dupId = fixture.psql("select id from identity.users where email = 'legacy-dup@example.com'");
fixture.psql(`
update public.profiles
set birth_date = '1997-08-08', reported_birth_time = '05:00',
birth_time_source = 'family_exact',
latitude = 36.420487, longitude = 114.209936, timezone_offset = 8,
birth_time_status = 'reported'
where id in ('${legacyId}', '${confirmedId}', '${collectId}', '${draftId}', '${dupId}');
-- Legacy session with activity + a user-accepted result (newest -> active).
insert into public.chat_sessions (id, user_id, title, theme, session_type, messages, updated_at)
values (
'aaaa1111-1111-4111-8111-111111111111', '${legacyId}', '旧校正A', 'general',
'birth_time_rectification',
'[{"role":"user","text":"2016年9月我离开家去北京工作"},{"role":"assistant","text":"已记录"}]',
now() - interval '1 day'
);
-- Legacy session with activity but no selection (older -> superseded).
insert into public.chat_sessions (id, user_id, title, theme, session_type, messages, updated_at)
values (
'bbbb1111-1111-4111-8111-111111111111', '${legacyId}', '旧校正B', 'general',
'birth_time_rectification',
'[{"role":"user","text":"2020年我开始担任管理职责"}]',
now() - interval '3 days'
);
-- Repeated empty legacy session (old -> abandoned).
insert into public.chat_sessions (id, user_id, title, theme, session_type, messages, updated_at)
values (
'cccc1111-1111-4111-8111-111111111111', '${legacyId}', '空校正', 'general',
'birth_time_rectification', '[]', now() - interval '10 days'
);
-- Engine-confirmed legacy session for the second user.
insert into public.chat_sessions (id, user_id, title, theme, session_type, messages, updated_at)
values (
'dddd1111-1111-4111-8111-111111111111', '${confirmedId}', '已确认校正', 'general',
'birth_time_rectification',
'[{"role":"user","text":"2015年我进入大学"}]',
now() - interval '2 days'
);
-- Messages but no results -> collecting_evidence (active).
insert into public.chat_sessions (id, user_id, title, theme, session_type, messages, updated_at)
values (
'eeee1111-1111-4111-8111-111111111111', '${collectId}', '收集校正', 'general',
'birth_time_rectification',
'[{"role":"user","text":"2018年我换了城市"}]',
now() - interval '1 day'
);
-- Single empty session -> draft.
insert into public.chat_sessions (id, user_id, title, theme, session_type, messages, updated_at)
values (
'ffff1111-1111-4111-8111-111111111111', '${draftId}', '新空校正', 'general',
'birth_time_rectification', '[]', now() - interval '1 day'
);
-- Duplicate empty sessions: newest -> draft, older -> abandoned.
insert into public.chat_sessions (id, user_id, title, theme, session_type, messages, updated_at)
values
('10111111-1111-4111-8111-111111111111', '${dupId}', '重复空1', 'general',
'birth_time_rectification', '[]', now() - interval '1 day'),
('20222222-2222-4222-8222-222222222222', '${dupId}', '重复空2', 'general',
'birth_time_rectification', '[]', now() - interval '2 days');
insert into public.agentic_rectification_results (
user_id, session_id, engine_result_id, canonical_input_hash, algorithm_version,
candidate_range, candidates, overall_confidence, selection_allowed, confirmation_allowed,
representative_time, selected_time, selection_kind, selected_at,
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 (
'${legacyId}', 'aaaa1111-1111-4111-8111-111111111111',
'legacy-engine-1', 'legacy-hash-1', 'legacy-v1',
'{"start_time":"04:00","end_time":"06:00"}',
'[{"rank":1,"time":"05:00","relative_support":70,"tied_minute_count":1}]',
'medium', true, false, '05:00', '05:00', 'user_accepted', now() - interval '1 day',
'1997-08-08', '05:00', 'family_exact', 10, 10, 36.420487, 114.209936, 8
);
insert into public.agentic_rectification_results (
user_id, session_id, engine_result_id, canonical_input_hash, algorithm_version,
candidate_range, candidates, overall_confidence, selection_allowed, confirmation_allowed,
representative_time, selected_time, selection_kind, selected_at,
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 (
'${confirmedId}', 'dddd1111-1111-4111-8111-111111111111',
'legacy-engine-2', 'legacy-hash-2', 'legacy-v1',
'{"start_time":"04:00","end_time":"06:00"}',
'[{"rank":1,"time":"05:00","relative_support":90,"tied_minute_count":1}]',
'high', true, true, '05:00', '05:00', 'engine_confirmed', now() - interval '2 days',
'1997-08-08', '05:00', 'family_exact', 10, 10, 36.420487, 114.209936, 8
);
`);
const service = createLocalPostgresDataClient(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
null,
"service_role",
);
// Preflight count: 8 legacy sessions without a V9 case.
assert.equal(
fixture.psql(`
select count(*) from public.chat_sessions
where session_type = 'birth_time_rectification'
and not exists (
select 1 from public.agentic_rectification_cases c where c.session_id = public.chat_sessions.id
)
`),
"8",
);
const backfill = await service.rpc("backfill_agentic_rectification_legacy_cases", {});
assert.equal(backfill.error, null, rpcError(backfill.error));
const stats = backfill.data as Record<string, unknown>;
assert.equal(stats.cases_created, 8);
assert.equal(stats.results_mapped, 2);
assert.equal(stats.confirmed, 1);
assert.equal(stats.candidate_accepted, 1);
assert.equal(stats.collecting_evidence, 1);
assert.equal(stats.draft, 2);
assert.equal(stats.abandoned, 2);
assert.equal(stats.superseded, 1);
// Statuses landed correctly.
assert.equal(
fixture.psql(`select status from public.agentic_rectification_cases where session_id = 'dddd1111-1111-4111-8111-111111111111'`),
"confirmed",
);
assert.equal(
fixture.psql(`select status from public.agentic_rectification_cases where session_id = 'ffff1111-1111-4111-8111-111111111111'`),
"draft",
);
assert.equal(
fixture.psql(`select status from public.agentic_rectification_cases where session_id = 'eeee1111-1111-4111-8111-111111111111'`),
"collecting_evidence",
);
assert.equal(
fixture.psql(`select status from public.agentic_rectification_cases where session_id = 'aaaa1111-1111-4111-8111-111111111111'`),
"candidate_accepted",
);
assert.equal(
fixture.psql(`select status from public.agentic_rectification_cases where session_id = 'bbbb1111-1111-4111-8111-111111111111'`),
"superseded",
);
assert.equal(
fixture.psql(`select status from public.agentic_rectification_cases where session_id = '10111111-1111-4111-8111-111111111111'`),
"draft",
);
assert.equal(
fixture.psql(`select status from public.agentic_rectification_cases where session_id = '20222222-2222-4222-8222-222222222222'`),
"abandoned",
);
// Exactly one resumable case per user.
assert.equal(
fixture.psql(`
select count(*) from (
select user_id from public.agentic_rectification_cases
where status in ('draft','collecting_evidence','candidate_ready','candidate_accepted','needs_rebaseline','paused')
group by user_id having count(*) > 1
) conflicts
`),
"0",
);
// Results are mapped to their cases.
assert.equal(
fixture.psql(`select count(*) from public.agentic_rectification_results where case_id is null`),
"0",
);
// Verification report is clean.
const verify = await service.rpc("verify_agentic_rectification_backfill", {});
assert.equal(verify.error, null, rpcError(verify.error));
const report = verify.data as Record<string, unknown>;
assert.equal(report.orphan_cases, 0);
assert.equal(report.resumable_conflicts, 0);
assert.equal(report.legacy_sessions_without_case, 0);
// Re-running the backfill creates nothing new (idempotent).
const again = await service.rpc("backfill_agentic_rectification_legacy_cases", {});
assert.equal(again.error, null, rpcError(again.error));
assert.equal((again.data as Record<string, unknown>).cases_created, 0);
} finally {
try {
await closeLocalPostgresDataPool(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
);
} finally {
fixture.stop();
}
}
});
test("v9 agent api migration applies, seeds the runtime flag and guards consent", { skip: skipWithoutDocker }, async () => {
const fixture = startPostgresFixture();
const schemaUrl = fixture.connectionUrl("schema_owner", "schema-owner-test-password");
const fixtureSkillSha256 = "d".repeat(64);
const fixtureSkillSourceCommit = "e".repeat(40);
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 20260813010000_agentic_rectification_v9_agent_api\.sql/);
assert.match(migration.stdout, /applied 20260813020000_rectification_birth_context_activity\.sql/);
assert.match(migration.stdout, /applied 20260811030000_feature_flags_admin_runtime_read_policy\.sql/);
// The runtime selector flag is published and enabled.
assert.equal(
fixture.psql(`select enabled || ':' || rollout_percentage || ':' || status
from public.feature_flags where flag_key = 'rectification_runtime_version'`),
"true:100:published",
);
assert.equal(
fixture.psqlAs(
"admin_runtime",
"admin-runtime-test-password",
`select enabled || ':' || rollout_percentage || ':' || status
from public.feature_flags
where flag_key = 'rectification_runtime_version'`,
),
"true:100:published",
);
// Run phases table exists with RLS.
assert.equal(
fixture.psql(`select count(*) from pg_tables where schemaname='public' and tablename='agentic_rectification_run_phases'`),
"1",
);
// Set up a profile + case + pending turn, then exercise the turn
// finalize and run-phase receipt RPCs. The identity row is seeded through
// the legitimate identity_runtime role (syncs to auth.users, satisfying
// the profiles FK); the public.* fixture rows are seeded through the
// postgres admin connection because identity_runtime has no grants on
// these tables (production least privilege). Tests must never widen
// runtime grants or change migrations.
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
insert into identity.users (id, name, email, email_verified, email_verified_at)
values ('66666666-6666-4666-8666-666666666666', 'V9 Agent API Fixture', 'v9-agent-api-fixture@example.com', true, now())
`);
fixture.psql(`
update public.profiles
set birth_date = '1997-08-08', reported_birth_time = '05:00', active_birth_time = null,
birth_time_source = 'family_exact', uncertainty_before_minutes = 10,
uncertainty_after_minutes = 10, birth_place_label = '河北省邯郸市武安市',
latitude = 36.420487, longitude = 114.209936,
timezone_id = 'Asia/Shanghai', timezone_offset = 8
where id = '66666666-6666-4666-8666-666666666666';
`);
fixture.psql(`
insert into public.chat_sessions (id, user_id, title, theme, session_type, messages)
values ('22222222-2222-4222-8222-222222222222', '66666666-6666-4666-8666-666666666666', '生时校正', 'general', 'birth_time_rectification', '[]'::jsonb);
`);
fixture.psql(`
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 (
'11111111-1111-4111-8111-111111111111', '66666666-6666-4666-8666-666666666666',
'22222222-2222-4222-8222-222222222222', 'draft', 'jyotish-birth-time-rectification', '9.0.0',
'${fixtureSkillSha256}', '${fixtureSkillSourceCommit}',
'${fingerprint}',
'{"birth_date":"1997-08-08","birth_place_label":"河北省邯郸市武安市","latitude":36.420487,"longitude":114.209936,"timezone_id":"Asia/Shanghai","timezone_offset":8,"birth_time_source":"family_exact"}'::jsonb,
'{"start_time":"04:50","end_time":"05:10"}'::jsonb
);
`);
fixture.psql(`
insert into public.agentic_rectification_turns (id, case_id, user_message, status, model_name)
values ('33333333-3333-4333-8333-333333333333', '11111111-1111-4111-8111-111111111111',
'2016年9月离开家去北京开始工作', 'pending', 'gpt-4o-mini');
`);
const service = createLocalPostgresDataClient(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
null,
"service_role",
);
const attempt = await service.rpc("create_agentic_rectification_run_attempt", {
p_user_id: "66666666-6666-4666-8666-666666666666",
p_case_id: "11111111-1111-4111-8111-111111111111",
p_turn_id: "33333333-3333-4333-8333-333333333333",
p_attempt_number: 1,
});
assert.equal(attempt.error, null, rpcError(attempt.error));
const attemptId = String((attempt.data as Record<string, unknown>).attempt_id);
const phase = await service.rpc("insert_agentic_rectification_run_phase", {
p_user_id: "66666666-6666-4666-8666-666666666666",
p_case_id: "11111111-1111-4111-8111-111111111111",
p_turn_id: "33333333-3333-4333-8333-333333333333",
p_phase: "skill.loaded",
p_tool_name: null,
p_sequence: 1,
p_attempt_id: attemptId,
});
assert.equal(phase.error, null, rpcError(phase.error));
const insertReceipt = (tool: string, status: string, methods: string[]) =>
service.rpc("insert_agentic_rectification_tool_receipt", {
p_user_id: "66666666-6666-4666-8666-666666666666",
p_case_id: "11111111-1111-4111-8111-111111111111",
p_turn_id: "33333333-3333-4333-8333-333333333333",
p_tool_name: tool,
p_public_phase: tool === "rectification-read-case" ? "case.loaded" : "candidates.comparing",
p_status: status,
p_input_fingerprint: null,
p_result_fingerprint: null,
p_engine_version: tool === "rectification-compare-candidates" ? "fixture-engine" : null,
p_safe_error_code: status === "failed" ? "fixture_failure" : null,
p_executed_methods: methods,
p_attempt_id: attemptId,
});
for (const result of [
await insertReceipt("rectification-read-case", "completed", []),
await insertReceipt("rectification-compare-candidates", "completed", [
"d1-rashi", "vimshottari-dasha", "d10-dashamsa",
]),
await insertReceipt("rectification-read-diagnostics", "failed", ["d9-navamsa"]),
]) {
assert.equal(result.error, null, rpcError(result.error));
}
const invalidMethod = await insertReceipt(
"rectification-compare-candidates",
"completed",
["private-technique"],
);
assert.match(rpcError(invalidMethod.error), /agentic_rectification_invalid_input/);
const skillReceipt = await service.rpc("insert_agentic_rectification_skill_run_receipt", {
p_user_id: "66666666-6666-4666-8666-666666666666",
p_case_id: "11111111-1111-4111-8111-111111111111",
p_turn_id: "33333333-3333-4333-8333-333333333333",
p_request_id: attemptId,
p_run_kind: "turn",
p_skill_name: "jyotish-birth-time-rectification",
p_skill_version: "9.0.0",
p_skill_sha256: fixtureSkillSha256,
p_source_commit: fixtureSkillSourceCommit,
});
assert.equal(skillReceipt.error, null, rpcError(skillReceipt.error));
for (const [publicPhase, sequence] of [
["skill.bound", 2],
["case.loaded", 3],
["intent.classified", 4],
["billing.settled", 5],
["run.completed", 6],
] as const) {
const lifecyclePhase = await service.rpc("insert_agentic_rectification_run_phase", {
p_user_id: "66666666-6666-4666-8666-666666666666",
p_case_id: "11111111-1111-4111-8111-111111111111",
p_turn_id: "33333333-3333-4333-8333-333333333333",
p_phase: publicPhase,
p_tool_name: null,
p_sequence: sequence,
p_attempt_id: attemptId,
});
assert.equal(lifecyclePhase.error, null, rpcError(lifecyclePhase.error));
}
const finalizedAttempt = await service.rpc("finalize_agentic_rectification_run_attempt", {
p_user_id: "66666666-6666-4666-8666-666666666666",
p_case_id: "11111111-1111-4111-8111-111111111111",
p_turn_id: "33333333-3333-4333-8333-333333333333",
p_attempt_id: attemptId,
p_status: "completed",
p_error_code: null,
p_usage: {},
});
assert.equal(finalizedAttempt.error, null, rpcError(finalizedAttempt.error));
const finalizedTurn = await service.rpc("finalize_agentic_rectification_turn", {
p_user_id: "66666666-6666-4666-8666-666666666666",
p_case_id: "11111111-1111-4111-8111-111111111111",
p_turn_id: "33333333-3333-4333-8333-333333333333",
p_attempt_id: attemptId,
p_status: "completed",
p_assistant_message: "好的,我会先核对出生时间范围。",
p_successful_attempt_id: attemptId,
});
assert.equal(finalizedTurn.error, null, rpcError(finalizedTurn.error));
const receipt = await service.rpc("get_agentic_rectification_turn_receipt", {
p_user_id: "66666666-6666-4666-8666-666666666666",
p_case_id: "11111111-1111-4111-8111-111111111111",
p_turn_id: "33333333-3333-4333-8333-333333333333",
});
assert.equal(receipt.error, null, rpcError(receipt.error));
const receiptRow = receipt.data as Record<string, unknown>;
assert.equal(receiptRow.status, "completed");
assert.equal(receiptRow.skill_version, "9.0.0");
assert.equal(receiptRow.engine_version, "fixture-engine");
assert.deepEqual(receiptRow.tools, [
"rectification-read-case",
"rectification-compare-candidates",
]);
assert.deepEqual(receiptRow.methods, [
"d1-rashi",
"d10-dashamsa",
"vimshottari-dasha",
]);
const consentCandidate = await service.rpc("persist_agentic_rectification_candidate_v2", {
p_user_id: "66666666-6666-4666-8666-666666666666",
p_case_id: "11111111-1111-4111-8111-111111111111",
p_engine_result_id: "agent-api-consent-engine",
p_evidence_ledger_fingerprint: "f".repeat(64),
p_candidate_range_fingerprint: "a".repeat(64),
p_skill_version: "9.0.0",
p_algorithm_version: "agent-api-consent-v2",
p_event_contract_version: "rectification-event-contract-v2",
p_decision_policy_version: "rectification-candidate-policy-v2",
p_candidate_range: { start_time: "04:50", end_time: "05:10" },
p_candidates: [{ rank: 1, time: "05:02", relative_support: 100, tied_minute_count: 1 }],
p_decision_receipt: {
display_allowed: true,
accept_allowed: true,
confirm_allowed: true,
representative_time: "05:02",
overall_confidence: "high",
margin_percent: 100,
},
p_execution_ledger: [{ phase: "decision.evaluate", status: "completed" }],
});
assert.equal(consentCandidate.error, null, rpcError(consentCandidate.error));
const consentCandidateRow = consentCandidate.data as Record<string, unknown>;
const consentCandidateId = String(
(consentCandidateRow.candidates as Array<Record<string, unknown>>)[0]?.candidate_id ?? "",
);
// Consent must be grounded in the current source turn before confirmation.
const consent = await service.rpc("confirm_agentic_rectification_candidate_for_case_v2", {
p_user_id: "66666666-6666-4666-8666-666666666666",
p_case_id: "11111111-1111-4111-8111-111111111111",
p_result_id: String(consentCandidateRow.result_id),
p_candidate_id: consentCandidateId,
p_request_id: "99999999-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
p_consent_quote: "就用05:02",
p_source_turn_id: "33333333-3333-4333-8333-333333333333",
});
assert.match(rpcError(consent.error), /agentic_rectification_consent_not_grounded/);
} finally {
try {
await closeLocalPostgresDataPool(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
);
} finally {
fixture.stop();
}
}
});
test("v9 allows changing an accepted minute within the same live candidate result", { skip: skipWithoutDocker }, 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);
const userId = "77777777-7777-4777-8777-777777777777";
const sessionId = "88888888-8888-4888-8888-888888888888";
const caseId = "99999999-9999-4999-8999-999999999999";
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
insert into identity.users (id, name, email, email_verified, email_verified_at)
values ('${userId}', 'Candidate Reselection Fixture', 'candidate-reselection@example.com', true, now())
`);
fixture.psql(`
update public.profiles
set birth_date = '1997-08-08',
reported_birth_time = '05:00',
active_birth_time = null,
birth_time = '05:00',
birth_time_status = 'reported',
birth_time_source = 'family_exact',
birth_time_period = null,
uncertainty_before_minutes = 10,
uncertainty_after_minutes = 10,
birth_place_label = '河北省邯郸市武安市',
latitude = 36.420487,
longitude = 114.209936,
timezone_id = 'Asia/Shanghai',
timezone_offset = 8
where id = '${userId}';
insert into public.chat_sessions (id, user_id, title, theme, session_type, messages)
values ('${sessionId}', '${userId}', '候选改选', 'general', 'birth_time_rectification', '[]'::jsonb);
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 (
'${caseId}', '${userId}', '${sessionId}', 'candidate_ready',
'${skillName}', '${skillVersion}', '${fingerprint}',
'${JSON.stringify(snapshot)}'::jsonb,
'${JSON.stringify(range)}'::jsonb
);
`);
const service = createLocalPostgresDataClient(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
null,
"service_role",
);
const persisted = await service.rpc("persist_agentic_rectification_candidate_v2", {
p_user_id: userId,
p_case_id: caseId,
p_engine_result_id: "candidate-reselection-engine",
p_evidence_ledger_fingerprint: "b".repeat(64),
p_candidate_range_fingerprint: "c".repeat(64),
p_skill_version: skillVersion,
p_algorithm_version: "candidate-reselection-v2",
p_event_contract_version: "rectification-event-contract-v2",
p_decision_policy_version: "rectification-candidate-policy-v2",
p_candidate_range: range,
p_candidates: [
{ rank: 1, time: "05:07", relative_support: 34, tied_minute_count: 1 },
{ rank: 2, time: "05:08", relative_support: 33, tied_minute_count: 2 },
{ rank: 3, time: "05:09", relative_support: 33, tied_minute_count: 2 },
],
p_decision_receipt: {
display_allowed: true,
accept_allowed: true,
confirm_allowed: true,
representative_time: "05:08",
overall_confidence: "low",
margin_percent: 1,
},
p_execution_ledger: [
{ phase: "candidate.score", status: "completed", engine: "fixture-engine" },
{ phase: "decision.evaluate", status: "completed", policy: "rectification-candidate-policy-v2" },
],
});
assert.equal(persisted.error, null, rpcError(persisted.error));
const persistedRow = persisted.data as Record<string, unknown>;
const resultId = String(persistedRow.result_id);
const persistedCandidates = persistedRow.candidates as Array<Record<string, unknown>>;
const candidateIdFor = (time: string) => String(
persistedCandidates.find((candidate) => candidate.time === time)?.candidate_id ?? "",
);
const firstCandidateId = candidateIdFor("05:07");
const switchedCandidateId = candidateIdFor("05:08");
const thirdCandidateId = candidateIdFor("05:09");
assert.match(firstCandidateId, /^[0-9a-f-]{36}$/);
assert.match(switchedCandidateId, /^[0-9a-f-]{36}$/);
assert.match(thirdCandidateId, /^[0-9a-f-]{36}$/);
const firstAccept = await service.rpc("accept_agentic_rectification_candidate_for_case_v2", {
p_user_id: userId,
p_case_id: caseId,
p_result_id: resultId,
p_candidate_id: firstCandidateId,
p_request_id: "11111111-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
});
assert.equal(firstAccept.error, null, rpcError(firstAccept.error));
const switchRequestId = "22222222-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
const switched = await service.rpc("accept_agentic_rectification_candidate_for_case_v2", {
p_user_id: userId,
p_case_id: caseId,
p_result_id: resultId,
p_candidate_id: switchedCandidateId,
p_request_id: switchRequestId,
});
assert.equal(switched.error, null, rpcError(switched.error));
assert.deepEqual(
{
saved_time: (switched.data as Record<string, unknown>).saved_time,
status: (switched.data as Record<string, unknown>).status,
case_status: (switched.data as Record<string, unknown>).case_status,
idempotent: (switched.data as Record<string, unknown>).idempotent,
},
{ saved_time: "05:08", status: "accepted", case_status: "candidate_accepted", idempotent: false },
);
assert.equal(
fixture.psql(`select active_birth_time || ':' || birth_time || ':' || birth_time_status from public.profiles where id = '${userId}'`),
"05:08:00:05:08:00:accepted",
);
assert.equal(
fixture.psql(`select selected_time || ':' || selection_kind || ':' || selected_candidate_id from public.agentic_rectification_results where id = '${resultId}'`),
`05:08:00:user_accepted:${switchedCandidateId}`,
);
assert.equal(
fixture.psql(`select accepted_time || ':' || status from public.agentic_rectification_cases where id = '${caseId}'`),
"05:08:00:candidate_accepted",
);
const replay = await service.rpc("accept_agentic_rectification_candidate_for_case_v2", {
p_user_id: userId,
p_case_id: caseId,
p_result_id: resultId,
p_candidate_id: switchedCandidateId,
p_request_id: switchRequestId,
});
assert.equal(replay.error, null, rpcError(replay.error));
assert.equal((replay.data as Record<string, unknown>).idempotent, true);
const consentTurn = await service.rpc("append_agentic_rectification_turn", {
p_user_id: userId,
p_case_id: caseId,
p_user_message: "我确认使用05:08作为出生时间",
p_assistant_message: "已记录你的确认。",
p_model_name: "test-model",
p_model_version: "1",
p_status: "completed",
p_request_id: "33333333-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
});
assert.equal(consentTurn.error, null, rpcError(consentTurn.error));
const consentTurnId = String((consentTurn.data as Record<string, unknown>).turn_id);
const confirmed = await service.rpc("confirm_agentic_rectification_candidate_for_case_v2", {
p_user_id: userId,
p_case_id: caseId,
p_result_id: resultId,
p_candidate_id: switchedCandidateId,
p_request_id: "44444444-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
p_consent_quote: "确认使用05:08",
p_source_turn_id: consentTurnId,
});
assert.equal(confirmed.error, null, rpcError(confirmed.error));
assert.equal((confirmed.data as Record<string, unknown>).status, "confirmed");
const confirmedSwitch = await service.rpc("accept_agentic_rectification_candidate_for_case_v2", {
p_user_id: userId,
p_case_id: caseId,
p_result_id: resultId,
p_candidate_id: thirdCandidateId,
p_request_id: "55555555-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
});
assert.match(rpcError(confirmedSwitch.error), /agentic_rectification_case_terminal/);
} finally {
try {
await closeLocalPostgresDataPool(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
);
} finally {
fixture.stop();
}
}
});