feat: model scene 9 staged messages
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
SCENE9_PHASE_PROMPTS,
|
||||
initialScene9MessageState,
|
||||
projectScene9Message,
|
||||
projectScene9SubmittedOverrides,
|
||||
scene9MessageReducer,
|
||||
} from "./scene9-message-state.js";
|
||||
|
||||
describe("scene 9 staged message state", () => {
|
||||
it("defines the exact prompt for every phase", () => {
|
||||
expect(SCENE9_PHASE_PROMPTS).toEqual({
|
||||
discover: "",
|
||||
draft: "Is the draft valid? Can you check and fix any issues?",
|
||||
validate: "",
|
||||
artifact: "Now save everything as a deployment and make sure the bindings are valid.",
|
||||
deployment:
|
||||
"The deployment lda_report_case_study.default is saved and valid. Shall we run it now?",
|
||||
});
|
||||
});
|
||||
|
||||
it("projects empty phases with a useful placeholder", () => {
|
||||
expect(projectScene9Message(initialScene9MessageState, "discover")).toMatchObject({
|
||||
draft: "",
|
||||
prefill: "",
|
||||
placeholder: expect.any(String),
|
||||
});
|
||||
expect(projectScene9Message(initialScene9MessageState, "validate")).toMatchObject({
|
||||
draft: "",
|
||||
prefill: "",
|
||||
placeholder: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it("projects the exact phase prefill without changing the draft", () => {
|
||||
expect(projectScene9Message(initialScene9MessageState, "draft")).toMatchObject({
|
||||
draft: "",
|
||||
prefill: SCENE9_PHASE_PROMPTS.draft,
|
||||
});
|
||||
expect(projectScene9Message(initialScene9MessageState, "artifact")).toMatchObject({
|
||||
draft: "",
|
||||
prefill: SCENE9_PHASE_PROMPTS.artifact,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves edited draft text exactly when submitting draft", () => {
|
||||
const edited = scene9MessageReducer(initialScene9MessageState, {
|
||||
type: "draft_edited",
|
||||
draft: " Check only the report binding. ",
|
||||
});
|
||||
|
||||
expect(scene9MessageReducer(edited, { type: "draft_submitted" })).toEqual({
|
||||
draft: edited.draft,
|
||||
submittedOverrides: { validate: edited.draft },
|
||||
runRequested: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("stores artifact submissions under the deployment destination", () => {
|
||||
const state = scene9MessageReducer(initialScene9MessageState, {
|
||||
type: "draft_edited",
|
||||
draft: "Save this edited deployment request",
|
||||
});
|
||||
|
||||
expect(scene9MessageReducer(state, { type: "artifact_submitted" })).toEqual({
|
||||
draft: state.draft,
|
||||
submittedOverrides: { deployment: state.draft },
|
||||
runRequested: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores blank submits and keeps duplicate submits idempotent", () => {
|
||||
const blank = { ...initialScene9MessageState, draft: " \n\t" };
|
||||
expect(scene9MessageReducer(blank, { type: "draft_submitted" })).toBe(blank);
|
||||
expect(scene9MessageReducer(blank, { type: "artifact_submitted" })).toBe(blank);
|
||||
expect(scene9MessageReducer(blank, { type: "run_requested" })).toBe(blank);
|
||||
|
||||
const submitted = scene9MessageReducer(
|
||||
scene9MessageReducer(initialScene9MessageState, {
|
||||
type: "draft_edited",
|
||||
draft: "A draft override",
|
||||
}),
|
||||
{ type: "draft_submitted" },
|
||||
);
|
||||
expect(scene9MessageReducer(submitted, { type: "draft_submitted" })).toBe(submitted);
|
||||
});
|
||||
|
||||
it("records the final run request without claiming execution", () => {
|
||||
const state = scene9MessageReducer(initialScene9MessageState, {
|
||||
type: "draft_edited",
|
||||
draft: "Run this deployment",
|
||||
});
|
||||
const requested = scene9MessageReducer(state, { type: "run_requested" });
|
||||
|
||||
expect(requested.runRequested).toBe("Run this deployment");
|
||||
expect(requested.submittedOverrides).toEqual({});
|
||||
expect(scene9MessageReducer(requested, { type: "run_requested" })).toBe(requested);
|
||||
});
|
||||
|
||||
it("projects only destination-phase overrides for the prepared thread", () => {
|
||||
const state = {
|
||||
...initialScene9MessageState,
|
||||
submittedOverrides: {
|
||||
validate: "Edited validation request",
|
||||
deployment: "Edited deployment request",
|
||||
},
|
||||
};
|
||||
|
||||
expect(projectScene9SubmittedOverrides(state)).toEqual({
|
||||
validate: "Edited validation request",
|
||||
deployment: "Edited deployment request",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
||||
|
||||
export type Scene9MessagePhase = AuthoringPhaseId;
|
||||
export type Scene9DestinationPhase = "validate" | "deployment";
|
||||
|
||||
export const SCENE9_PHASE_PROMPTS: Readonly<Record<Scene9MessagePhase, string>> = {
|
||||
discover: "",
|
||||
draft: "Is the draft valid? Can you check and fix any issues?",
|
||||
validate: "",
|
||||
artifact: "Now save everything as a deployment and make sure the bindings are valid.",
|
||||
deployment:
|
||||
"The deployment lda_report_case_study.default is saved and valid. Shall we run it now?",
|
||||
};
|
||||
|
||||
export const SCENE9_PHASE_PLACEHOLDERS: Readonly<Record<Scene9MessagePhase, string>> = {
|
||||
discover: "Ask about the workflow authoring process.",
|
||||
draft: "Review the prepared draft.",
|
||||
validate: "Ask about the draft validation results.",
|
||||
artifact: "Save the validated workflow as a deployment.",
|
||||
deployment: "Ask whether the saved deployment should run.",
|
||||
};
|
||||
|
||||
export type Scene9SubmittedOverrides = Readonly<
|
||||
Partial<Record<Scene9DestinationPhase, string>>
|
||||
>;
|
||||
|
||||
export type Scene9MessageState = {
|
||||
readonly draft: string;
|
||||
readonly submittedOverrides: Scene9SubmittedOverrides;
|
||||
readonly runRequested: string | null;
|
||||
};
|
||||
|
||||
export const initialScene9MessageState: Scene9MessageState = {
|
||||
draft: "",
|
||||
submittedOverrides: {},
|
||||
runRequested: null,
|
||||
};
|
||||
|
||||
export type Scene9MessageAction =
|
||||
| { readonly type: "draft_edited"; readonly draft: string }
|
||||
| { readonly type: "draft_submitted" }
|
||||
| { readonly type: "artifact_submitted" }
|
||||
| { readonly type: "run_requested" };
|
||||
|
||||
const hasText = (text: string): boolean => text.trim().length > 0;
|
||||
|
||||
const submitOverride = (
|
||||
state: Scene9MessageState,
|
||||
destination: Scene9DestinationPhase,
|
||||
): Scene9MessageState => {
|
||||
if (!hasText(state.draft) || state.submittedOverrides[destination] !== undefined) return state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
submittedOverrides: {
|
||||
...state.submittedOverrides,
|
||||
[destination]: state.draft,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const scene9MessageReducer = (
|
||||
state: Scene9MessageState,
|
||||
action: Scene9MessageAction,
|
||||
): Scene9MessageState => {
|
||||
switch (action.type) {
|
||||
case "draft_edited":
|
||||
return state.runRequested === null ? { ...state, draft: action.draft } : state;
|
||||
case "draft_submitted":
|
||||
return submitOverride(state, "validate");
|
||||
case "artifact_submitted":
|
||||
return submitOverride(state, "deployment");
|
||||
case "run_requested":
|
||||
return state.runRequested === null && hasText(state.draft)
|
||||
? { ...state, runRequested: state.draft }
|
||||
: state;
|
||||
}
|
||||
};
|
||||
|
||||
export type Scene9MessageProjection = {
|
||||
readonly draft: string;
|
||||
readonly prefill: string;
|
||||
readonly placeholder: string;
|
||||
};
|
||||
|
||||
export const projectScene9Message = (
|
||||
state: Scene9MessageState,
|
||||
phase: Scene9MessagePhase,
|
||||
): Scene9MessageProjection => ({
|
||||
draft: state.draft,
|
||||
prefill: SCENE9_PHASE_PROMPTS[phase],
|
||||
placeholder: SCENE9_PHASE_PLACEHOLDERS[phase],
|
||||
});
|
||||
|
||||
/** Keeps the transcript-facing map limited to the two staged handoff destinations. */
|
||||
export const projectScene9SubmittedOverrides = (
|
||||
state: Scene9MessageState,
|
||||
): Scene9SubmittedOverrides => state.submittedOverrides;
|
||||
Reference in New Issue
Block a user