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
112 changes: 112 additions & 0 deletions apps/server/src/conversation-plan/corrections-contributions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { describe, expect, test } from "bun:test";
import type { ConversationPlan } from "@chopin/protocol";
import { initialState, replay, restoreState } from "./domain";
import { buildTargetingRequest, QUESTION_SET_VERSION } from "./questions";
import { extractQuotes } from "./quotes";
import { add, correct, message, open, stance } from "./corrections.test-fixtures";

// Original callbacks/data: archive 446a9779a937fa5be7cd3eb52fd7f3023d691ed2,
// apps/server/src/conversation-plan/corrections.test.ts.

describe("conversation card contribution corrections", () => {
test("retargets reasons and constraints with exact wording and source attribution", () => {
let state = add(add(open(initialState()), "option", "a"), "option", "b");
state = add(state, "reason", "reason", "a");
state = add(state, "constraint", "constraint", "t1");
let original = structuredClone(state.threads[0].contributions.slice(-2));
state = correct(state, "reason-to-thread", {
kind: "retarget-contribution",
contributionId: "reason",
targetId: "t1",
});
state = correct(state, "constraint-to-b", {
kind: "retarget-contribution",
contributionId: "constraint",
targetId: "b",
});
let changed = state.threads[0].contributions.slice(-2);
for (let index = 0; index < 2; index++) {
expect(changed[index]).toEqual({
...original[index],
targetId: index === 0 ? "t1" : "b",
targetEditedBy: "bob",
});
}
expect(replay(state.events).threads).toEqual(state.threads);
expect(restoreState(JSON.parse(JSON.stringify(state))).threads).toEqual(
JSON.parse(JSON.stringify(state.threads)),
);
let forged = JSON.parse(JSON.stringify(state));
forged.threads[0].contributions.find((item: { id: string }) => item.id === "reason")
.targetEditedBy = "mallory";
expect(() => restoreState(forged)).toThrow(/snapshot/i);
let invalid = structuredClone(state);
(invalid.events.at(-1) as any).change.extra = "unknown";
expect(() => restoreState(invalid)).toThrow(/unknown/i);
});

test("rejects wrong contribution kinds, foreign targets, overlong IDs and stale versions", () => {
let state = add(open(initialState()), "option", "a");
state = add(state, "reason", "reason", "a");
state = open(state, "t2");
state = add(state, "option", "foreign", undefined, "t2");
let before = structuredClone(state);
for (
let change of [
{ kind: "retarget-contribution", contributionId: "a", targetId: "t1" },
{ kind: "retarget-contribution", contributionId: "reason", targetId: "t2" },
{ kind: "retarget-contribution", contributionId: "reason", targetId: "foreign" },
{ kind: "retarget-contribution", contributionId: "reason", targetId: "a" },
{ kind: "retarget-contribution", contributionId: "reason", targetId: "x".repeat(201) },
] as ConversationPlan.CorrectionChange[]
) {
expect(() => correct(state, "invalid-target", change)).toThrow();
}
expect(() =>
correct(state, "stale-target", {
kind: "retarget-contribution",
contributionId: "reason",
targetId: "t1",
}, state.threads[0].version - 1)
).toThrow(/stale/i);
expect(state).toEqual(before);
});

test("later Jev context uses bounded corrected targets and current stances", () => {
let state = add(add(open(initialState()), "option", "a"), "reason", "reason", "t1");
for (let index = 0; index < 15; index++) {
state = add(state, "reason", `other-${index}`, "t1");
}
state = stance(state, "chat020-stance", "a");
state = correct(state, "retarget-chat020", {
kind: "retarget-stance",
stanceId: "chat020-stance",
});
state = correct(state, "retarget-reason", {
kind: "retarget-contribution",
contributionId: "reason",
targetId: "a",
});
let next = message("next", "What follows from that?");
let request = buildTargetingRequest(next, [], state.threads, extractQuotes(next.text));
let context = (request.state as {
threads: Array<{
stances: Array<{ participant: string; optionId?: string; position: string }>;
contributions: Array<{ id: string; targetId: string; relation: string }>;
}>;
}).threads[0];
expect(QUESTION_SET_VERSION).toBe("conversation-plan-8");
expect(context.stances).toEqual([{
participant: "alice",
optionId: undefined,
position: "oppose",
}]);
expect(context.contributions).toHaveLength(4);
expect(context.contributions[0]).toMatchObject({
id: "reason",
targetId: "a",
relation: "qualifies",
});
expect(state.events.some((event) => event.id === "chat020-stance")).toBe(true);
});
});
108 changes: 108 additions & 0 deletions apps/server/src/conversation-plan/corrections-excerpts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { describe, expect, test } from "bun:test";
import type { ConversationPlan } from "@chopin/protocol";
import { initialState } from "./domain";
import {
add,
bob,
correctExcerpt,
message,
open,
reviewedExcerpt,
} from "./corrections.test-fixtures";

// Original callbacks/data: archive 446a9779a937fa5be7cd3eb52fd7f3023d691ed2,
// apps/server/src/conversation-plan/corrections.test.ts.

describe("conversation card excerpt corrections", () => {
test.each(
[
{
kind: "option",
text: "Postmark handles email delivery.",
targetOptionId: undefined,
targetId: "t1",
},
{ kind: "reason", text: "SQS is operationally simple.", targetOptionId: "a", targetId: "a" },
{
kind: "constraint",
text: "Must stay inside our VPC.",
targetOptionId: undefined,
targetId: "t1",
},
] as const,
)("adds a held $kind excerpt with original source and human correction actor", ({
kind,
text,
targetOptionId,
targetId,
}) => {
let state = add(open(initialState()), "option", "a");
let original = message("held-chat", `I think ${text} Please review.`);
let start = original.text.indexOf(text);
let end = start + text.length;
state = reviewedExcerpt(state, original, 0, original.text.length, "ignored");
let action = {
actionId: `correct-${kind}`,
threadId: "t1",
expectedVersion: state.threads[0]!.version,
change: {
kind: "add-excerpt",
messageId: original.id,
start,
end,
contributionKind: kind,
targetOptionId,
},
} as unknown as ConversationPlan.CorrectionAction;
let changed = correctExcerpt(state, action, original);
let added = changed.threads[0]!.contributions.at(-1)!;
let excerpt = text;
let sourceRef = {
messageId: original.id,
author: original.author,
quote: excerpt,
start,
end,
role: kind,
};
expect(added).toMatchObject({
kind,
text: excerpt,
targetId,
authoring: "quoted",
actor: bob,
sources: [sourceRef],
});
expect(changed.events.at(-1)).toMatchObject({
type: `${kind}.added`,
origin: "human",
actor: bob,
source: sourceRef,
contribution: { text: excerpt, targetId },
});
expect(changed.analysis.at(-1)?.outcomes?.[0]?.status).toBe("ignored");
expect(changed.revision).toBe(state.revision + 1);
});

test("replaying an excerpt correction action ID is idempotent", () => {
let state = add(open(initialState()), "option", "a");
let original = message("held-retry", "SQS is operationally simple.");
state = reviewedExcerpt(state, original, 0, original.text.length);
let action = {
actionId: "correct-retry",
threadId: "t1",
expectedVersion: state.threads[0]!.version,
change: {
kind: "add-excerpt",
messageId: original.id,
start: 0,
end: original.text.length,
contributionKind: "reason",
targetOptionId: "a",
},
} as unknown as ConversationPlan.CorrectionAction;
let changed = correctExcerpt(state, action, original);
expect(correctExcerpt(changed, action, original)).toBe(changed);
expect(changed.events.filter(event => event.origin === "human")).toHaveLength(1);
});
});
128 changes: 128 additions & 0 deletions apps/server/src/conversation-plan/corrections.test-fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type { Chat, ConversationPlan } from "@chopin/protocol";
import { applyCorrection, applyInference } from "./domain";
import { QUESTION_SET_VERSION } from "./questions";

// Exact actor/helper declarations: archive 446a9779a937fa5be7cd3eb52fd7f3023d691ed2,
// apps/server/src/conversation-plan/corrections.test.ts.
export let alice = { kind: "member", handle: "alice" } as const;
export let bob = { kind: "member", handle: "bob" } as const;

export function message(id: string, text: string): Chat.Entry {
return { id, text, author: alice, ts: 1 };
}

export function source(
entry: Chat.Entry,
role: ConversationPlan.SourceRole,
): ConversationPlan.SourceRef {
return {
messageId: entry.id,
author: entry.author as ConversationPlan.SourceAuthor,
quote: entry.text,
start: 0,
end: entry.text.length,
role,
};
}

export function open(state: ConversationPlan.State, threadId = "t1"): ConversationPlan.State {
let entry = message(`open-${threadId}`, `Which option for ${threadId}?`);
return applyInference(state, {
id: `open-event-${threadId}`,
type: "thread.opened",
threadId,
observedThreadVersion: 0,
origin: "classifier",
actor: { kind: "classifier" },
at: 1,
source: source(entry, "question"),
question: entry.text,
}, entry);
}

export function add(
state: ConversationPlan.State,
kind: "option" | "reason" | "constraint",
id: string,
targetId?: string,
threadId = "t1",
): ConversationPlan.State {
let entry = message(`message-${id}`, `Words for ${id}`);
return applyInference(state, {
id: `event-${id}`,
type: `${kind}.added`,
threadId,
observedThreadVersion: state.threads.find((thread) => thread.id === threadId)!.version,
origin: "classifier",
actor: { kind: "classifier" },
at: 2,
source: source(entry, kind),
contribution: { id, text: entry.text, authoring: "quoted", targetId, relation: "qualifies" },
}, entry);
}

export function stance(
state: ConversationPlan.State,
id: string,
optionId?: string,
): ConversationPlan.State {
let entry = message(`message-${id}`, `I object to ${optionId ?? "the thread"}.`);
return applyInference(state, {
id,
type: "stance.changed",
threadId: "t1",
observedThreadVersion: state.threads[0].version,
origin: "classifier",
actor: { kind: "classifier" },
at: 3,
source: source(entry, "objection"),
optionId,
position: "oppose",
}, entry);
}

export function correct(
state: ConversationPlan.State,
actionId: string,
change: ConversationPlan.CorrectionChange,
expectedVersion = state.threads[0].version,
): ConversationPlan.State {
return applyCorrection(state, { actionId, threadId: "t1", expectedVersion, change }, bob, 10);
}

export function reviewedExcerpt(
state: ConversationPlan.State,
entry: Chat.Entry,
start: number,
end: number,
status: "review" | "ignored" = "review",
): ConversationPlan.State {
return {
...state,
revision: state.revision + 1,
analysis: [...state.analysis, {
messageId: entry.id,
questionSetVersion: QUESTION_SET_VERSION,
modelVersion: "jev-test",
status: "unlinked",
passes: [],
eventIds: [],
outcomes: [{ start, end, status, gate: "unclassified excerpt", eventIds: [] }],
}],
};
}

export function correctExcerpt(
state: ConversationPlan.State,
action: ConversationPlan.CorrectionAction,
entry: Chat.Entry,
): ConversationPlan.State {
let apply = applyCorrection as unknown as (
state: ConversationPlan.State,
action: ConversationPlan.CorrectionAction,
actor: typeof bob,
at: number,
messages: readonly Chat.Entry[],
) => ConversationPlan.State;
return apply(state, action, bob, 10, [entry]);
}
Loading
Loading