163 lines
7.3 KiB
TypeScript
163 lines
7.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildPlaybookFromRecording,
|
|
promptInvokesSkill,
|
|
reconcilePlaybookFromRun,
|
|
} from "./teach-playbook.js";
|
|
|
|
describe("promptInvokesSkill", () => {
|
|
it("matches an explicit request to run the skill", () => {
|
|
expect(promptInvokesSkill("run Export weekly CRM list", "Export weekly CRM list")).toBe(true);
|
|
expect(promptInvokesSkill("Please use the Export CRM skill now", "Export CRM")).toBe(true);
|
|
});
|
|
|
|
it("ignores prompts that only mention the name in passing", () => {
|
|
expect(promptInvokesSkill("export the notes to markdown", "Export")).toBe(false);
|
|
expect(promptInvokesSkill("run the exporter script", "Export")).toBe(false);
|
|
expect(promptInvokesSkill("run a report", "Export weekly CRM list")).toBe(false);
|
|
});
|
|
|
|
it("skips prompts that already carry the playbook and very short names", () => {
|
|
expect(promptInvokesSkill("Run taught skill: Export\nSteps:", "Export")).toBe(false);
|
|
expect(promptInvokesSkill("run cs now", "cs")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("buildPlaybookFromRecording", () => {
|
|
it("turns pointer and typing events into steps", () => {
|
|
const playbook = buildPlaybookFromRecording(
|
|
"Export weekly CRM list",
|
|
[
|
|
{ at: "2026-01-01T00:00:00.000Z", kind: "pointer", x: 120, y: 40, type: "click" },
|
|
{ at: "2026-01-01T00:00:01.000Z", kind: "clipboard", text: "weekly-export.csv" },
|
|
],
|
|
[{ at: "2026-01-01T00:00:02.000Z", summary: "Export dialog open" }],
|
|
);
|
|
|
|
expect(playbook.steps.some((step) => step.includes("Click"))).toBe(true);
|
|
expect(playbook.steps.some((step) => step.includes("weekly-export.csv"))).toBe(true);
|
|
expect(playbook.steps.some((step) => step.includes("Export dialog open"))).toBe(false);
|
|
expect(playbook.howToCheck).toContain("Export dialog open");
|
|
expect(playbook.whenToUse).toContain("Export weekly CRM list");
|
|
expect(playbook.approvalBoundaries.length).toBeGreaterThan(0);
|
|
expect(playbook.failureHandling.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("fills approval and failure fields when the demo lacked them", () => {
|
|
const playbook = buildPlaybookFromRecording("Save a note", []);
|
|
expect(playbook.approvalBoundaries).toContain("approval");
|
|
expect(playbook.failureHandling).toContain("stop");
|
|
expect(playbook.steps.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("redacts obvious password-like input", () => {
|
|
const playbook = buildPlaybookFromRecording("Sign in", [
|
|
{ at: "2026-01-01T00:00:00.000Z", kind: "clipboard", text: "my-password" },
|
|
]);
|
|
expect(playbook.steps.join(" ")).toContain("[redacted input]");
|
|
});
|
|
|
|
it("coalesces consecutive typed characters and keeps repeated keys", () => {
|
|
const playbook = buildPlaybookFromRecording("Search", [
|
|
{ at: "2026-01-01T00:00:00.000Z", kind: "key", key: "b" },
|
|
{ at: "2026-01-01T00:00:00.100Z", kind: "key", key: "o" },
|
|
{ at: "2026-01-01T00:00:00.200Z", kind: "key", key: "o" },
|
|
{ at: "2026-01-01T00:00:00.300Z", kind: "key", key: "k" },
|
|
{ at: "2026-01-01T00:00:00.400Z", kind: "key", key: "Enter" },
|
|
]);
|
|
expect(playbook.steps).toEqual(['Type "book".', "Press key: Enter."]);
|
|
});
|
|
|
|
it("keeps spaces typed during a demo", () => {
|
|
const playbook = buildPlaybookFromRecording("Search", [
|
|
{ at: "2026-01-01T00:00:00.000Z", kind: "key", key: "h" },
|
|
{ at: "2026-01-01T00:00:00.100Z", kind: "key", key: "i" },
|
|
{ at: "2026-01-01T00:00:00.200Z", kind: "key", key: " " },
|
|
{ at: "2026-01-01T00:00:00.300Z", kind: "key", key: "t" },
|
|
{ at: "2026-01-01T00:00:00.400Z", kind: "key", key: "h" },
|
|
{ at: "2026-01-01T00:00:00.500Z", kind: "key", key: "e" },
|
|
{ at: "2026-01-01T00:00:00.600Z", kind: "key", key: "r" },
|
|
{ at: "2026-01-01T00:00:00.700Z", kind: "key", key: "e" },
|
|
]);
|
|
expect(playbook.steps).toEqual(['Type "hi there".']);
|
|
});
|
|
|
|
it("redacts typed credentials the same way as clipboard input", () => {
|
|
const playbook = buildPlaybookFromRecording("Sign in", [
|
|
{ at: "2026-01-01T00:00:00.000Z", kind: "key", key: "p" },
|
|
{ at: "2026-01-01T00:00:00.100Z", kind: "key", key: "a" },
|
|
{ at: "2026-01-01T00:00:00.200Z", kind: "key", key: "s" },
|
|
{ at: "2026-01-01T00:00:00.300Z", kind: "key", key: "s" },
|
|
{ at: "2026-01-01T00:00:00.400Z", kind: "key", key: "w" },
|
|
{ at: "2026-01-01T00:00:00.500Z", kind: "key", key: "o" },
|
|
{ at: "2026-01-01T00:00:00.600Z", kind: "key", key: "r" },
|
|
{ at: "2026-01-01T00:00:00.700Z", kind: "key", key: "d" },
|
|
]);
|
|
expect(playbook.steps.join(" ")).toContain("[redacted input]");
|
|
expect(playbook.steps.join(" ")).not.toContain("password");
|
|
});
|
|
|
|
it("coalesces a press-and-release into a click and keeps a drag", () => {
|
|
const click = buildPlaybookFromRecording("Open", [
|
|
{ at: "2026-01-01T00:00:00.000Z", kind: "pointer", x: 10, y: 20, type: "down" },
|
|
{ at: "2026-01-01T00:00:00.050Z", kind: "pointer", x: 10, y: 20, type: "up" },
|
|
]);
|
|
expect(click.steps).toEqual(["Click left button at (10, 20)."]);
|
|
|
|
const drag = buildPlaybookFromRecording("Move", [
|
|
{ at: "2026-01-01T00:00:00.000Z", kind: "pointer", x: 10, y: 20, type: "down" },
|
|
{ at: "2026-01-01T00:00:00.050Z", kind: "pointer", x: 80, y: 90, type: "move" },
|
|
{ at: "2026-01-01T00:00:00.080Z", kind: "pointer", x: 80, y: 90, type: "up" },
|
|
]);
|
|
expect(drag.steps).toEqual(["Drag left button from (10, 20) to (80, 90)."]);
|
|
});
|
|
|
|
it("records scroll steps from a demo", () => {
|
|
const playbook = buildPlaybookFromRecording("Scroll the list", [
|
|
{ at: "2026-01-01T00:00:00.000Z", kind: "scroll", type: "down", text: "3" },
|
|
]);
|
|
expect(playbook.steps).toEqual(["Scroll down 3 times."]);
|
|
});
|
|
|
|
it("includes semantic UI anchors with coordinate fallback when captured", () => {
|
|
const playbook = buildPlaybookFromRecording("Export report", [
|
|
{
|
|
at: "2026-01-01T00:00:00.000Z",
|
|
kind: "pointer",
|
|
type: "click",
|
|
x: 420,
|
|
y: 180,
|
|
targetRole: "button",
|
|
targetLabel: "Export CSV",
|
|
selector: "#export-csv",
|
|
},
|
|
]);
|
|
expect(playbook.steps).toEqual([
|
|
'Click button "Export CSV" [selector: #export-csv] (fallback left button at (420, 180)).',
|
|
]);
|
|
});
|
|
|
|
it("self-heals a playbook step and updates verification summary via reconcilePlaybookFromRun", () => {
|
|
const initial = buildPlaybookFromRecording("Export report", [
|
|
{ at: "2026-01-01T00:00:00.000Z", kind: "pointer", type: "click", x: 100, y: 50 },
|
|
{ at: "2026-01-01T00:00:01.000Z", kind: "key", key: "Enter" },
|
|
]);
|
|
const healed = reconcilePlaybookFromRun(initial, {
|
|
failedStepIndex: 0,
|
|
replacementSteps: [
|
|
'Click button "More actions" [selector: #more-menu] (fallback left button at (140, 50)).',
|
|
'Click menuitem "Export" [selector: #export-item] (fallback left button at (140, 90)).',
|
|
],
|
|
verifiedSnapshotSummary: "Export completed banner visible",
|
|
});
|
|
expect(healed.steps).toEqual([
|
|
'Click button "More actions" [selector: #more-menu] (fallback left button at (140, 50)).',
|
|
'Click menuitem "Export" [selector: #export-item] (fallback left button at (140, 90)).',
|
|
"Press key: Enter.",
|
|
]);
|
|
expect(healed.howToCheck).toBe(
|
|
"The final screen should match: Export completed banner visible.",
|
|
);
|
|
});
|
|
});
|