Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions apps/server/src/conversation-plan/interpret.test-fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type { ConversationPlan } from "@chopin/protocol";
import { applyInference, initialState } from "./domain";
import type { JevQuestion } from "./jev";
import { optionIdFor } from "./policy";
import { message } from "./policy-initial.test-fixtures";
export { message } from "./policy-initial.test-fixtures";
export { stateWithOption } from "./policy-terminal.test-fixtures";

// Exact archive446a9779a937fa5be7cd3eb52fd7f3023d691ed2, pipeline.test.ts.
// Complete callbacks and helpers retained; injected offline transport only.
export let seededOptionId = optionIdFor("channel", "seed-option", 0, 1_000);

export function seeded(): ConversationPlan.State {
let state = initialState();
let original = message("m1", "Should we use an optional outline?");
return applyInference(state, {
id: "open",
type: "thread.opened",
threadId: "thread-a",
observedThreadVersion: 0,
origin: "classifier",
actor: { kind: "classifier" },
at: 1000,
source: {
messageId: original.id,
author: original.author as ConversationPlan.SourceAuthor,
quote: original.text,
start: 0,
end: original.text.length,
role: "question",
},
question: original.text,
}, original);
}

export function withOption(state: ConversationPlan.State): ConversationPlan.State {
let proposal = message("seed-option", "Use an optional outline.");
return applyInference(state, {
id: "seed-option-event",
type: "option.added",
threadId: "thread-a",
observedThreadVersion: state.threads[0].version,
origin: "classifier",
actor: { kind: "classifier" },
at: 1000,
source: {
messageId: proposal.id,
author: proposal.author as ConversationPlan.SourceAuthor,
quote: proposal.text,
start: 0,
end: proposal.text.length,
role: "option",
},
contribution: {
id: seededOptionId,
text: proposal.text,
authoring: "quoted",
targetId: "thread-a",
},
}, proposal);
}

export function settledBy(state: ConversationPlan.State, handle: string): ConversationPlan.State {
let withFirstOption = withOption(state);
let proposal = message("settle-proposal", "Let's just go with it.", handle);
return applyInference(withFirstOption, {
id: "settle-proposal-event",
type: "settle.suggested",
threadId: "thread-a",
observedThreadVersion: withFirstOption.threads[0].version,
origin: "classifier",
actor: { kind: "classifier" },
at: proposal.ts,
source: {
messageId: proposal.id,
author: proposal.author as ConversationPlan.SourceAuthor,
quote: proposal.text,
start: 0,
end: proposal.text.length,
role: "resolution",
},
optionId: seededOptionId,
}, proposal);
}

export function mockResult(
questions: Record<string, JevQuestion>,
overrides: Record<string, string | number>,
): any {
let answers: Record<string, any> = {};
let distributions: Record<string, Record<string, number>> = {};
for (let [key, question] of Object.entries(questions)) {
if (question.type === "noul") {
answers[key] = {
type: "noul",
noul: typeof overrides[key] === "number"
? overrides[key]
: key.endsWith("_owned_unretracted")
? 0.95
: 0.05,
};
}
if (question.type === "choice") {
let choice = typeof overrides[key] === "string" ? overrides[key] : "none";
if (!(choice in question.criteria)) choice = Object.keys(question.criteria)[0];
let probabilities = Object.fromEntries(
Object.keys(question.criteria).map((
option,
) => [
option,
option === choice ? 0.95 : 0.05 / (Object.keys(question.criteria).length - 1),
]),
);
answers[key] = { type: "choice", choice, confidence: 0.95, probabilities };
}
if (question.type === "score") {
let chosen = typeof overrides[key] === "number" ? Math.round(overrides[key]) : 0;
let probabilities = Object.fromEntries(
question.criteria.map((_, i) => [String(i), i === chosen ? 1 : 0]),
);
answers[key] = {
type: "score",
score: chosen,
confidence: 1,
legend: Object.fromEntries(question.criteria.map((label, i) => [String(i), label])),
probabilities,
};
}
let answer = answers[key];
distributions[key] = answer.type === "noul"
? { yes: answer.noul, no: 1 - answer.noul }
: answer.probabilities;
}
return {
model: "mock-jev",
answers,
distributions,
usage: { input_tokens: 1, output_tokens: 1 },
latencyMs: 1,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { expect, test } from "bun:test";
import { planEvents } from "./policy";
import { extractQuotes } from "./quotes";
import { confidentChoice, first, follow, message } from "./policy-initial.test-fixtures";
import { stateWithOption } from "./policy-terminal.test-fixtures";

test("declarative option capture refuses attribution, negation, withdrawal, ambiguity, and repeats", () => {
let state = stateWithOption(
"How should we provide agent access?",
"access-thread",
"existing-access",
"Use a hosted gateway.",
);
let duplicateState = stateWithOption(
"How should we provide agent access?",
"access-thread",
"existing-access",
"Copilot.",
);
let plain = "By agent access I mean Copilot or bring-your-own API keys.";
let assess = (
text: string,
overrides: {
first?: Record<string, any>;
second?: Record<string, any>;
triage?: Record<string, any>;
} = {},
candidateState = state,
) => {
let current = message(`guard-${text}`, text);
return planEvents({
channelId: "channel",
message: current,
state: candidateState,
first: {
...first({ new_option: 0.96 }),
thread_target: confidentChoice("access-thread"),
...overrides.triage,
},
candidates: extractQuotes(text).map((quote, index) => ({
...quote,
answers: {
...follow({ role: "none", thread: "access-thread" }),
new_option: { type: "noul", noul: 0.93 },
duplicate: { type: "noul", noul: 0.04 },
...(index ? overrides.second : overrides.first),
},
})),
});
};
for (
let text of [
"Bob said, by agent access I mean Copilot or bring-your-own API keys.",
"By agent access I mean not Copilot or bring-your-own API keys.",
`${plain} Actually, I take that back.`,
"By agent access I mean Copilot or Copilot.",
]
) {
expect(extractQuotes(text)).not.toEqual([
{ quote: "Copilot", start: 23, end: 30 },
{ quote: "bring-your-own API keys", start: 34, end: 57 },
]);
expect(assess(text).events).toEqual([]);
}
expect(assess(plain, {}, duplicateState).events).toEqual([]);
expect(assess(plain, { triage: { thread_target: confidentChoice("new") } }).events)
.toEqual([]);
expect(
assess(plain, { triage: { c1_owned_unretracted: { type: "noul", noul: 0.2 } } })
.events,
).toEqual([]);
expect(assess(plain, { second: { thread: confidentChoice("none") } }).events).toEqual([]);
expect(
assess(plain, { second: { duplicate: { type: "noul", noul: 0.95 } } })
.events,
).toEqual([]);
});
103 changes: 103 additions & 0 deletions apps/server/src/conversation-plan/pipeline-commitment-pending.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { expect, test } from "bun:test";
import type { JevQuestion } from "./jev";
import {
buildCandidateTargetingRequest,
buildTargetingRequest,
buildTriageRequest,
QUESTION_SET_VERSION,
} from "./questions";
import { extractQuotes } from "./quotes";
import { seeded, settledBy } from "./interpret.test-fixtures";
import { message } from "./policy-initial.test-fixtures";

test("mixed-message assent names the unique pending choice in its own targeting questions", () => {
let current = message(
"mixed-assent",
"Sounds good to me. What about agent access? Copilot? BYO API keys?",
"alice",
);
let prior = message("prior-proposal", "Let's just go with the optional outline.", "bob");
let request = buildTargetingRequest(
current,
[prior],
settledBy(seeded(), "bob").threads,
extractQuotes(current.text),
);
let state = request.state as {
recent: Array<{ text: string }>;
candidates: Array<{ quote: string }>;
};
expect(state.recent.at(-1)?.text).toBe(prior.text);
expect(state.candidates.map(candidate => candidate.quote)).toEqual([
"Sounds good to me.",
"What about agent access?",
"Copilot?",
"BYO API keys?",
]);
let target = request.questions.c0_thread as Extract<JevQuestion, { type: "choice" }>;
expect(target.instructions).toContain("candidates[0].quote");
expect(target.instructions).toContain("later clauses");
expect(target.criteria["thread-a"].toLowerCase()).toContain("pending proposal to settle");
expect(target.criteria["thread-a"]).toContain("Use an optional outline.");
let agreement = request.questions.c0_agrees_with_settle;
expect(agreement?.instructions).toContain("candidates[0].quote");
expect(agreement?.instructions).toContain("Use an optional outline.");
expect(agreement?.instructions).toContain("bob");
expect(agreement?.instructions).toContain("later clauses");
expect(agreement?.instructions).toContain("quoted");
expect(agreement?.instructions).toContain("sarcasm");
});

test("v5 triage checks exact quote ownership while targeting isolates each clause", () => {
let current = message(
"mixed-assent",
"Sounds good to me. What about agent access? Copilot? BYO API keys?",
"Jules",
);
let recent = [
message("purpose", "We need to figure out auth.", "Mina"),
message("alternatives", "Auth0, custom login, or GitHub auth?", "Jules"),
message("proposal", "Let's just go with GitHub.", "Mina"),
];
let quotes = extractQuotes(current.text);
let state = settledBy(seeded(), "Mina");
let triage = buildTriageRequest(current, recent, state.threads, quotes);
expect(QUESTION_SET_VERSION).toBe("conversation-plan-8");
expect(Object.keys(triage.questions).filter(key => key.endsWith("_owned_unretracted")))
.toEqual([
"c0_owned_unretracted",
"c1_owned_unretracted",
"c2_owned_unretracted",
"c3_owned_unretracted",
]);
expect(
(triage.state as { candidates: Array<{ quote: string; start: number; end: number }> })
.candidates,
).toEqual(quotes);
for (let index = 0; index < quotes.length; index++) {
let request = buildCandidateTargetingRequest(
current,
recent,
state.threads,
quotes,
index,
);
let visible = request.state as {
current: { id: string; text: string };
preceding?: string;
recent: Array<{ id: string }>;
candidates: Array<{ quote: string; start: number; end: number }>;
};
expect(Object.keys(request.questions).every(key => key.startsWith(`c${index}_`)))
.toBe(true);
expect(visible.current).toMatchObject({ id: current.id, text: quotes[index]!.quote });
expect(visible.candidates.at(-1)).toEqual(quotes[index]);
expect(visible.candidates).toHaveLength(index + 1);
expect(visible.recent.map(entry => entry.id)).toEqual(recent.map(entry => entry.id));
expect(visible.preceding ?? "").toBe(
current.text.slice(0, quotes[index]!.start)
.slice(-300),
);
if (index < 3) expect(JSON.stringify(request.state)).not.toContain("BYO API keys?");
}
});
Loading
Loading