Files
Jyotisha/frontend/tests/rectification-v9-database.test.ts
T
Jesse_Chen f708edf365
Independent Staging Quality Gate / validate (push) Successful in 10m5s
Independent Staging Quality Gate / publish (push) Successful in 7m4s
fix(rectification): ingest first-turn events without dropping day precision or claiming a unique minute
SQL kinds now match the TypeScript ledger so batch ingest can confirm dated events. Confirm no longer burns the opening focus, recap uses server date labels, and the Agent sees indistinguishable width instead of a fake unique minute.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 07:44:23 +08:00

1279 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 without aborting the RPC.
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.equal(ungrounded.error, null, rpcError(ungrounded.error));
assert.equal((ungrounded.data as Record<string, unknown>).outcome, "rejected");
assert.equal((ungrounded.data as Record<string, unknown>).error_code, "quote_not_grounded");
assert.equal((ungrounded.data as Record<string, unknown>).evidence_id, null);
// 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();
}
}
});