refactor(memory): extract F8 promotion gate (#22)
This commit is contained in:
@@ -1,67 +1,156 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const {
|
||||
dedupePromotionCandidates,
|
||||
promotionOptions,
|
||||
promotionContent,
|
||||
splitPromotionChoices,
|
||||
} = require("../../tht-gate.js");
|
||||
const { installMemoryGate } = require("../memory/index.js");
|
||||
const { createFakePi } = require("./fake_pi_runtime.js");
|
||||
|
||||
// F8 memory-promotion gate: the candidates are DETERMINISTIC (computed by
|
||||
// `tht memory promote --preview --json`), the model only names the session.
|
||||
// These tests pin the pure candidate->widget mapping and the choice partition.
|
||||
|
||||
const CANDIDATES = [
|
||||
{ decision_seq: 3, type: "table_promoted", subject: "fact_seeablazione",
|
||||
detail: "tabella principale ablazioni", rationale: "scelta dal reviewer",
|
||||
question_context: "quante ablazioni nel 2023", tables: ["fact_seeablazione"], concepts: [] },
|
||||
{ decision_seq: 5, type: "concept_clarified", subject: "paziente attivo",
|
||||
detail: "flag_attivo = TRUE", rationale: "",
|
||||
question_context: "quante ablazioni nel 2023", tables: [], concepts: ["paziente attivo"] },
|
||||
];
|
||||
const TABLE_CANDIDATE = {
|
||||
decision_seq: 3,
|
||||
type: "table_promoted",
|
||||
subject: "fact_seeablazione",
|
||||
detail: "tabella principale ablazioni",
|
||||
rationale: "scelta dal reviewer",
|
||||
question_context: "quante ablazioni nel 2023",
|
||||
tables: ["fact_seeablazione"],
|
||||
concepts: [],
|
||||
};
|
||||
|
||||
test("promotionOptions maps candidates to seq-keyed options", () => {
|
||||
assert.deepEqual(promotionOptions(dedupePromotionCandidates(CANDIDATES)), [
|
||||
const MEMORY_CANDIDATE = {
|
||||
decision_seq: 5,
|
||||
type: "concept_clarified",
|
||||
subject: "paziente attivo",
|
||||
detail: "flag_attivo = TRUE",
|
||||
rationale: "scelta dal reviewer",
|
||||
question_context: "quante ablazioni nel 2023",
|
||||
tables: [],
|
||||
concepts: ["paziente attivo"],
|
||||
};
|
||||
|
||||
|
||||
test("the public Memory facade owns F8 policy and mutation ordering", async () => {
|
||||
const { pi, tools, ctx } = createFakePi();
|
||||
const calls = [];
|
||||
let descriptor;
|
||||
const duplicateMemory = { ...MEMORY_CANDIDATE, decision_seq: 6 };
|
||||
const declinedMemory = {
|
||||
...MEMORY_CANDIDATE,
|
||||
decision_seq: 7,
|
||||
subject: "ricovero indice",
|
||||
detail: "first_event",
|
||||
};
|
||||
ctx.ui.input = async (title) => {
|
||||
descriptor = JSON.parse(title);
|
||||
calls.push(["review", descriptor.options.map((option) => option.id)]);
|
||||
return JSON.stringify({ id: descriptor.id, choices: ["seq-5"] });
|
||||
};
|
||||
|
||||
installMemoryGate(pi, {
|
||||
workflow: {
|
||||
activate: () => calls.push(["activate"]),
|
||||
phase: () => ({ number: 8, id: "F8" }),
|
||||
close: (_ctx, _session, phaseNumber, summary) => {
|
||||
calls.push(["close", phaseNumber, summary]);
|
||||
return null;
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
execute: (_ctx, args) => {
|
||||
calls.push(["memory-execute", args]);
|
||||
return JSON.stringify([
|
||||
TABLE_CANDIDATE,
|
||||
MEMORY_CANDIDATE,
|
||||
duplicateMemory,
|
||||
declinedMemory,
|
||||
]);
|
||||
},
|
||||
mutate: (_ctx, args, recovery) => {
|
||||
calls.push(["memory-mutate", args, recovery]);
|
||||
return null;
|
||||
},
|
||||
},
|
||||
ledger: {
|
||||
record: (_ctx, session, decision, recovery) => {
|
||||
calls.push(["ledger", session, decision, recovery]);
|
||||
return null;
|
||||
},
|
||||
},
|
||||
waitForReviewer: async (runtimeContext, widget) => {
|
||||
const response = await runtimeContext.ui.input(JSON.stringify(widget), "");
|
||||
return JSON.parse(response);
|
||||
},
|
||||
toTextResult: (text) => ({ content: [{ type: "text", text }] }),
|
||||
});
|
||||
|
||||
const result = await tools.get("reviewer_memory_promote").def.execute(
|
||||
"promote-via-facade",
|
||||
{ session: "s1" },
|
||||
null,
|
||||
null,
|
||||
ctx,
|
||||
);
|
||||
|
||||
assert.deepEqual(descriptor.options, [
|
||||
{
|
||||
id: "seq-5",
|
||||
label: "concept_clarified: paziente attivo",
|
||||
detail: "flag_attivo = TRUE",
|
||||
rationale: "",
|
||||
rationale: "scelta dal reviewer",
|
||||
meta: { question_context: "quante ablazioni nel 2023" },
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
id: "seq-7",
|
||||
label: "concept_clarified: ricovero indice",
|
||||
detail: "first_event",
|
||||
rationale: "scelta dal reviewer",
|
||||
meta: { question_context: "quante ablazioni nel 2023" },
|
||||
selected: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("F8 proposes semantically identical memory content only once", () => {
|
||||
const duplicate = { ...CANDIDATES[0], decision_seq: 9 };
|
||||
assert.deepEqual(
|
||||
dedupePromotionCandidates([CANDIDATES[0], duplicate, CANDIDATES[1]])
|
||||
.map((candidate) => candidate.decision_seq),
|
||||
[5],
|
||||
);
|
||||
});
|
||||
|
||||
test("F8 never proposes promoted tables as memory", () => {
|
||||
const candidates = dedupePromotionCandidates(CANDIDATES);
|
||||
const options = promotionOptions(candidates);
|
||||
const c = promotionContent(candidates);
|
||||
|
||||
assert.deepEqual(candidates.map((candidate) => candidate.type), ["concept_clarified"]);
|
||||
assert.deepEqual(options.map((option) => option.id), ["seq-5"]);
|
||||
assert.ok(!c.includes("fact_seeablazione"));
|
||||
assert.ok(c.includes("flag_attivo = TRUE"));
|
||||
assert.ok(c.includes("quante ablazioni nel 2023"));
|
||||
});
|
||||
|
||||
test("splitPromotionChoices partitions by selection", () => {
|
||||
const candidates = dedupePromotionCandidates(CANDIDATES);
|
||||
const { promote, decline } = splitPromotionChoices(candidates, ["seq-5"]);
|
||||
assert.deepEqual(promote.map((c) => c.decision_seq), [5]);
|
||||
assert.deepEqual(decline.map((c) => c.decision_seq), []);
|
||||
});
|
||||
|
||||
test("empty or missing choices declines everything", () => {
|
||||
const candidates = dedupePromotionCandidates(CANDIDATES);
|
||||
assert.equal(splitPromotionChoices(candidates, []).decline.length, 1);
|
||||
assert.equal(splitPromotionChoices(candidates, undefined).decline.length, 1);
|
||||
assert.deepEqual(descriptor.selected, ["seq-5", "seq-7"]);
|
||||
assert.doesNotMatch(descriptor.content, /fact_seeablazione/);
|
||||
assert.match(descriptor.content, /flag_attivo = TRUE/);
|
||||
assert.deepEqual(calls.slice(0, 7), [
|
||||
["activate"],
|
||||
[
|
||||
"memory-execute",
|
||||
["promote", "--session", "s1", "--preview", "--json"],
|
||||
],
|
||||
["review", ["seq-5", "seq-7"]],
|
||||
[
|
||||
"memory-mutate",
|
||||
["save-one", "--session", "s1", "--decision", "5", "--json"],
|
||||
"Recupero manuale (umano): tht memory save-one --session s1 " +
|
||||
"--decision 5. Finora salvate: 0.",
|
||||
],
|
||||
[
|
||||
"ledger",
|
||||
"s1",
|
||||
{
|
||||
type: "memory_promoted",
|
||||
subject: "paziente attivo",
|
||||
detail: "seq:5",
|
||||
rationale: "scelta dal reviewer",
|
||||
},
|
||||
"Memoria salvata nel vectordb ma decisione memory_promoted NON registrata: " +
|
||||
"recupero manuale (umano) con tht decision add --session s1 " +
|
||||
"--type memory_promoted --subject \"paziente attivo\" --detail seq:5.",
|
||||
],
|
||||
[
|
||||
"ledger",
|
||||
"s1",
|
||||
{
|
||||
type: "memory_promotion_declined",
|
||||
subject: "ricovero indice",
|
||||
detail: "seq:7",
|
||||
},
|
||||
"",
|
||||
],
|
||||
[
|
||||
"close",
|
||||
8,
|
||||
"Promozione registrata: 1 memorie salvate nel vectordb, 1 candidati scartati.",
|
||||
],
|
||||
]);
|
||||
assert.match(result.content[0].text, /1 memorie salvate.*1 candidati scartati/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user