journeys

package
v1.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package journeys is the declarative model of a user journey: a named sequence of UI actions, each with assertions about the resulting screen and evidence to capture. A journey is authored once and run deterministically — it is how a UI regression or an acceptance test is expressed as code rather than a prose script.

A journey is written in a closed vocabulary of verbs, selectors, subjects and operators, specified normatively in docs/journey-taxonomy.md and expressed as data in vocabulary.go. It names no MCP tool: Compile lowers each verb to the tool call that expresses it, so the tool surface can change without invalidating a checked-in suite, and so a journey's reach can be derived from the document rather than declared by it.

A journey compiles to a plan.Document (see Compile), so it runs through the same executor that Apply uses: every step is evaluated by the policy engine, audited, and fail-stopped on the first failure. An assertion is compiled to a call of the Assert tool, so a failed assertion is a failed step — the journey stops and reports it, exactly as a test runner would.

This package is a leaf: it depends only on the plan model and the standard library, and holds no MCP or Windows types. That is what lets `journey validate` check a document completely — verbs, parameters, selectors, the operator matrix — offline, in CI, on any platform. The live run lives in internal/winmcp, which has the engine and the tool surface.

Index

Constants

View Source
const (
	DefaultWaitTimeout  = 10.0
	MaxWaitTimeout      = 120.0
	DefaultWaitInterval = 0.4
	MinWaitInterval     = 0.05
)

Wait bounds and defaults, matching the WaitFor tool's own limits so a journey cannot ask for a poll the executor will silently clamp.

View Source
const MaxPatternLength = 512

MaxPatternLength caps a regex. RE2 does not backtrack, so a pathological pattern cannot hang a run; the cap is about a human being able to review it.

View Source
const MaxPauseSeconds = 60.0

MaxPauseSeconds bounds a fixed sleep, matching the Wait tool's clamp.

View Source
const RedactedPlaceholder = "sign in (redacted — name the stored credential before running)"

RedactedPlaceholder is the step name used where a secure typed run was dropped. The step is emitted as an enter_credential carrying no credential name, so the captured keystrokes never reach the file and the draft says what to supply instead — a stored credential the agent can use but never read.

View Source
const SchemaVersion = 2

SchemaVersion is the journey document version this build understands.

Version 1 is not accepted and there is no conversion. A v1 step named an MCP tool and carried an untyped argument map, which is precisely what this schema replaces; converting one mechanically would produce a document that still could not be checked.

Variables

View Source
var (
	ErrJourneyVersion  = errors.New("unsupported journey version")
	ErrInvalidJourney  = errors.New("invalid journey")
	ErrUnknownVerb     = errors.New("unknown verb")
	ErrUnknownSubject  = errors.New("unknown assertion subject")
	ErrUnknownOperator = errors.New("unknown assertion operator")
	ErrBadOccurrence   = errors.New(`occurrence must be "unique", "first", or a non-negative integer`)
)

Errors surfaced by loading and validation, distinct so a caller can report a precise cause.

Functions

func Compile

func Compile(j Journey, sessionID string) (plan.Document, error)

Compile turns a validated journey into a plan.Document. It is CompileWithOrigins without the provenance, for callers that only need to run the plan.

Types

type Assertion

type Assertion struct {
	Subject  Subject   `json:"subject"`
	Target   *Selector `json:"target,omitempty"`
	Operator Operator  `json:"operator"`
	// Expected is the value compared against, typed by the subject: a string for a
	// text subject, a number for a numeric one, a bool for a boolean one, and an
	// array of those for is_one_of. An operator taking no operand rejects it.
	Expected any `json:"expected,omitempty"`
	// Message is a human description of what is being verified, carried through to
	// the assertion's PASS/FAIL line whether or not it polled.
	Message string `json:"message,omitempty"`
	// Wait turns a single evaluation into a polled one. Absent, the condition is
	// checked once.
	Wait *Wait `json:"wait,omitempty"`

	// Comparison modifiers, all defaulting off so behaviour is never implicit.
	IgnoreCase         bool `json:"ignore_case,omitempty"`
	Trim               bool `json:"trim,omitempty"`
	CollapseWhitespace bool `json:"collapse_whitespace,omitempty"`
}

Assertion is one condition checked after a step, as subject × operator × expected. A failed one fails the step, which stops the run — a journey is a test, so it stops at the first thing that is not true.

type ElementFacts added in v1.3.0

type ElementFacts struct {
	Value    string
	Checked  bool
	Selected bool
	Enabled  bool
	Expanded bool

	HasValue          bool
	HasToggle         bool
	HasSelection      bool
	HasInvoke         bool
	HasExpandCollapse bool
}

ElementFacts is what the accessibility tree reported about an element at the moment of capture: which patterns it supports, and what they currently hold.

Pattern availability is the difference between recording what was clicked and recording what the click meant. A checkbox and a button look the same to a mouse hook; only the tree knows one has a toggle state.

type Event

type Event struct {
	Kind EventKind

	// Click fields.
	X, Y int // physical-pixel screen coordinates of the click
	// AutomationID is the developer-assigned id of the clicked element, when the
	// application sets one. It is the top rung of the selector ladder: unlike the
	// accessible name it survives translation.
	AutomationID string
	Name         string // resolved accessible name of the clicked element ("" if none)
	ControlType  string // e.g. Button, Edit, ListItem
	Button       string // left | right | middle (default left)
	Double       bool   // a double-click
	// Facts is what the element could do and held at capture time. Empty when the
	// tree reported nothing, in which case inference falls back to the control type.
	Facts ElementFacts

	// Char fields.
	Char rune // the typed rune
	// Secure marks a keystroke into a password-class field. Secure runs are
	// redacted: their characters are never written to the journey. This is the
	// recorder's one security-critical signal, pinned by a never-contains-secret test.
	Secure bool

	// Key fields.
	Key string // the named key: "Enter", "Tab", "Escape", "Backspace", …
}

Event is one captured user action. The OS-side recorder fills the fields relevant to its Kind; the emitter reads only those.

type EventKind

type EventKind string

EventKind is the class of a captured input event.

const (
	// EventClick is a mouse click resolved to a UI element (or bare coordinates).
	EventClick EventKind = "click"
	// EventChar is one typed character. Consecutive chars coalesce into a type_text.
	EventChar EventKind = "char"
	// EventKey is a non-text key press (Enter, Tab, Escape, …) that does not
	// coalesce into typed text.
	EventKey EventKind = "key"
	// EventAssert is the author pointing at something and saying it matters. It
	// carries no action: it becomes an assertion on the step being recorded.
	EventAssert EventKind = "assert"
)

type Journey

type Journey struct {
	Version     int    `json:"version"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Steps       []Step `json:"steps"`
	// ExpectedEvidence lists evidence labels the journey is expected to produce, so
	// a reviewer can state up front what artifacts a passing run must contain. Every
	// entry must be captured by some step, or the expectation can never be met — and
	// a run that does not actually capture one fails, even if every assertion passed.
	ExpectedEvidence []string `json:"expected_evidence,omitempty"`
}

Journey is a named, ordered sequence of UI steps with assertions and evidence.

func Emit

func Emit(name string, events []Event) Journey

Emit compiles a captured event stream into a Journey. Consecutive typed characters coalesce into one type_text step; a secure run becomes a single enter_credential step carrying no secret. Clicks become the verb their control type implies, targeting the element by the most stable key it offers. A named key becomes press_keys, except a trailing Enter, which folds into the preceding text step as submit.

The result is a reviewable draft: the recorder captures actions, not intent, so the human who recorded it confirms the steps and adds the assertions that make it a test.

func Parse

func Parse(raw []byte) (Journey, error)

Parse decodes a journey document, rejecting unknown fields so a typo in a key surfaces at load rather than being silently dropped.

func (Journey) Validate

func (j Journey) Validate() error

Validate checks a journey against the closed vocabulary: a supported version, a name, at least one step, every verb known and carrying exactly the parameters it takes, every selector well-formed, every assertion a legal subject/operator pair with a correctly typed expected value, and every expected-evidence label actually captured by some step.

It reports every problem at once, so an author fixing a file does not discover them one run at a time. Everything it checks is checkable offline: the verbs carry their own types, so no tool schema — and therefore no Windows host — is needed, and `journey validate` is a CI check rather than a smoke test.

type NameMatch added in v1.3.0

type NameMatch string

NameMatch is how a selector's name is compared. It qualifies name and belongs only with it: an automation_id is an identifier, matched exactly, with nothing to relax.

const (
	// MatchExact is the default. The exact-then-substring fallback it replaces is
	// what let a selector for "Save" resolve to "Save As…" on any screen with no
	// exact match — and pass, having acted on the wrong control.
	MatchExact    NameMatch = "exact"
	MatchContains NameMatch = "contains"
	MatchMatches  NameMatch = "matches"
)

func (NameMatch) Known added in v1.3.0

func (m NameMatch) Known() bool

Known reports whether the name-match mode is one this build resolves.

type Occurrence added in v1.3.0

type Occurrence struct {
	Mode  OccurrenceMode
	Index int
}

Occurrence is a small union: the JSON is either "unique", "first", or an integer. It is a type rather than an `any` so the rest of the package reads a mode instead of type-switching on a decoded value.

func (Occurrence) MarshalJSON added in v1.3.0

func (o Occurrence) MarshalJSON() ([]byte, error)

MarshalJSON writes the mode name, or the bare index for OccurrenceIndex, so a document round-trips to the form it was written in.

func (Occurrence) Resolved added in v1.3.0

func (o Occurrence) Resolved() Occurrence

Resolved returns the occurrence with its default applied.

func (Occurrence) String added in v1.3.0

func (o Occurrence) String() string

String renders an occurrence for a step label or a run record.

func (*Occurrence) UnmarshalJSON added in v1.3.0

func (o *Occurrence) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts "unique", "first", or a non-negative integer.

type OccurrenceMode added in v1.3.0

type OccurrenceMode string

OccurrenceMode is how a selector resolves multiple matches.

const (
	// OccurrenceUnique is the default: more than one match is a failure naming the
	// candidates, rather than a silent pick whose outcome depends on tree ordering.
	OccurrenceUnique OccurrenceMode = "unique"
	// OccurrenceFirst takes the first match, when an author means to.
	OccurrenceFirst OccurrenceMode = "first"
	// OccurrenceIndex takes a specific 0-based match.
	OccurrenceIndex OccurrenceMode = "index"
)

type Operator added in v1.3.0

type Operator string

Operator is the comparison an assertion applies.

const (
	OpIs             Operator = "is"
	OpIsNot          Operator = "is_not"
	OpContains       Operator = "contains"
	OpDoesNotContain Operator = "does_not_contain"
	OpStartsWith     Operator = "starts_with"
	OpEndsWith       Operator = "ends_with"
	OpMatches        Operator = "matches"
	OpDoesNotMatch   Operator = "does_not_match"
	OpIsEmpty        Operator = "is_empty"
	OpIsNotEmpty     Operator = "is_not_empty"
	OpIsOneOf        Operator = "is_one_of"
	OpIsNotOneOf     Operator = "is_not_one_of"
	OpIsTrue         Operator = "is_true"
	OpIsFalse        Operator = "is_false"
	OpExists         Operator = "exists"
	OpDoesNotExist   Operator = "does_not_exist"
	OpGreaterThan    Operator = "greater_than"
	OpGreaterOrEqual Operator = "greater_or_equal"
	OpLessThan       Operator = "less_than"
	OpLessOrEqual    Operator = "less_or_equal"
)

func (Operator) Known added in v1.3.0

func (o Operator) Known() bool

Known reports whether the operator is one this build evaluates.

type Origin added in v1.3.0

type Origin struct {
	// StepIndex is the journey step this came from, or -1 for a step that belongs
	// to no journey step.
	StepIndex int
	Kind      OriginKind
	// Index is the position among the step's assertions or captures.
	Index int

	Verb     Verb
	Selector *Selector

	Subject  Subject
	Operator Operator
	Expected any
	Message  string
	Wait     *Wait

	Label string
}

Origin records which part of the journey one compiled plan step came from. Compilation flattens a journey — an action, then its assertions, then its captures — so without this the executor sees an undifferentiated list of tool calls and cannot attribute a result back to what the author wrote.

It is returned alongside the document rather than carried on it: a plan step's fields are hashed into the plan id, and journey provenance is not part of what an approval binds to.

func CompileWithOrigins added in v1.3.0

func CompileWithOrigins(j Journey, sessionID string) (plan.Document, []Origin, error)

CompileWithOrigins turns a validated journey into a plan.Document and the per-step provenance: each step becomes the tool call its verb lowers to, preceded by a perception step where one is needed, followed by one step per assertion and one per evidence capture.

The result is an ordinary plan, so it runs through the same executor Apply uses: every step is policy-evaluated, audited as plan.step, and fail-stopped on the first failure. A failed assertion is an Assert tool error — a failed step — so the journey stops there, which is exactly a test runner's behaviour.

The origins slice is parallel to doc.Steps and is what lets the run record attribute a span to the verb or assertion an author wrote.

sessionID is stamped onto the document so its audit and any evidence bundle tie back to the run; it does not affect the plan id, which is content-derived.

type OriginKind added in v1.3.0

type OriginKind string

OriginKind says what part of a journey a compiled plan step came from.

const (
	// OriginAction is the step's verb.
	OriginAction OriginKind = "action"
	// OriginObserve is a compiler-inserted perception step.
	OriginObserve OriginKind = "observe"
	// OriginAssertion is one of the step's assertions.
	OriginAssertion OriginKind = "assertion"
	// OriginEvidence is one of the step's evidence captures.
	OriginEvidence OriginKind = "evidence"
)

type Scope added in v1.3.0

type Scope string

Scope is how wide a selector searches.

const (
	ScopeForeground Scope = "foreground"
	ScopeAnyWindow  Scope = "any_window"
)

func (Scope) Known added in v1.3.0

func (s Scope) Known() bool

Known reports whether the scope is one this build resolves.

type Selector added in v1.3.0

type Selector struct {
	AutomationID string `json:"automation_id,omitempty"`
	Name         string `json:"name,omitempty"`
	ControlType  string `json:"control_type,omitempty"`
	// NameMatch qualifies Name and belongs only with it. Empty means MatchExact.
	NameMatch NameMatch `json:"name_match,omitempty"`
	// Occurrence decides what happens when the selector matches more than one
	// element. Empty means OccurrenceUnique, under which ambiguity is a failure.
	//
	// omitzero, not omitempty: omitempty has no effect on a struct field, so the
	// zero value would be written out as an occurrence of "" and fail to parse back
	// — which is exactly what a recorded journey does on its round trip to disk.
	Occurrence Occurrence `json:"occurrence,omitzero"`
	// Point is a coordinate target: legal, because a recorder must be able to
	// capture a control with no name, and marked non-durable wherever it appears.
	Point []int `json:"point,omitempty"`
	// Scope is how wide the search is. Empty means ScopeForeground.
	Scope Scope `json:"scope,omitempty"`
}

Selector names the UI element or window a verb acts on. Exactly one of AutomationID, Name or Point identifies it; ControlType may narrow any of them.

The three identifying keys are a stability ladder (taxonomy §4.1): AutomationID is developer-assigned and survives translation, Name survives relayout, and Point survives nothing. A recorder picks the highest rung the application offers.

func (*Selector) Label added in v1.3.0

func (s *Selector) Label() string

Label renders a selector compactly: its identifying key, which is also the name the change manifest shows for what the step touches.

type Step

type Step struct {
	Name string `json:"name,omitempty"`
	Verb Verb   `json:"verb"`

	// Target selects the element or window the verb acts on.
	Target *Selector `json:"target,omitempty"`

	App        string  `json:"app,omitempty"`        // open_app
	Window     string  `json:"window,omitempty"`     // focus_window, resize_window, close_window
	URL        string  `json:"url,omitempty"`        // navigate
	Value      string  `json:"value,omitempty"`      // set_value
	Text       string  `json:"text,omitempty"`       // type_text
	Keys       string  `json:"keys,omitempty"`       // press_keys
	Credential string  `json:"credential,omitempty"` // enter_credential
	Label      string  `json:"label,omitempty"`      // capture
	Direction  string  `json:"direction,omitempty"`  // scroll
	Amount     int     `json:"amount,omitempty"`     // scroll
	Seconds    float64 `json:"seconds,omitempty"`    // pause
	Submit     bool    `json:"submit,omitempty"`     // type_text, enter_credential
	Scope      Scope   `json:"scope,omitempty"`      // observe
	Position   []int   `json:"position,omitempty"`   // resize_window
	Size       []int   `json:"size,omitempty"`       // resize_window

	// Assertions lists conditions checked after the action; a failed one fails the
	// step. Named to match the plural-noun convention every other array field uses.
	Assertions []Assertion `json:"assertions,omitempty"`
	// Evidence lists captions for evidence to capture after the action.
	Evidence []string `json:"evidence,omitempty"`
}

Step is one action, the assertions that must hold after it, and any evidence to capture at that point.

The action's parameters are typed and flat rather than an argument bag: each verb declares which of them it takes (see verbs in vocabulary.go), so passing `value` to `press_keys` is a validation error offline, before anything runs. A parameter left at its zero value counts as absent, which is why every optional parameter is one whose zero value means "not asked for".

type Subject added in v1.3.0

type Subject string

Subject names what an assertion reads.

const (
	SubjectScreenText         Subject = "screen.text"
	SubjectWindowTitle        Subject = "window.title"
	SubjectWindow             Subject = "window"
	SubjectElement            Subject = "element"
	SubjectElementName        Subject = "element.name"
	SubjectElementValue       Subject = "element.value"
	SubjectElementControlType Subject = "element.control_type"
	SubjectElementEnabled     Subject = "element.enabled"
	SubjectElementChecked     Subject = "element.checked"
	SubjectElementSelected    Subject = "element.selected"
	SubjectElementFocused     Subject = "element.focused"
	SubjectElementCount       Subject = "element.count"
	SubjectResultText         Subject = "result.text"
)

func (Subject) Known added in v1.3.0

func (s Subject) Known() bool

Known reports whether the subject is one this build evaluates.

type ValueType added in v1.3.0

type ValueType string

ValueType is the type of the value a subject reads. It is what decides which operators are legal (taxonomy §5.3).

const (
	TypeText      ValueType = "text"
	TypeNumber    ValueType = "number"
	TypeBoolean   ValueType = "boolean"
	TypeExistence ValueType = "existence"
)

type Verb added in v1.3.0

type Verb string

Verb is one action a journey step performs. The set is closed: it is the whole vocabulary a journey may be written in (taxonomy §3).

const (
	// Lifecycle.
	VerbOpenApp      Verb = "open_app"
	VerbFocusWindow  Verb = "focus_window"
	VerbResizeWindow Verb = "resize_window"
	VerbCloseWindow  Verb = "close_window"

	// Navigation.
	VerbNavigate Verb = "navigate"
	VerbScroll   Verb = "scroll"

	// Direct manipulation.
	VerbClick       Verb = "click"
	VerbDoubleClick Verb = "double_click"
	VerbRightClick  Verb = "right_click"
	VerbHover       Verb = "hover"

	// Control operations, via UIA patterns.
	VerbInvoke   Verb = "invoke"
	VerbToggle   Verb = "toggle"
	VerbSelect   Verb = "select"
	VerbExpand   Verb = "expand"
	VerbCollapse Verb = "collapse"

	// Text entry.
	VerbSetValue  Verb = "set_value"
	VerbTypeText  Verb = "type_text"
	VerbClear     Verb = "clear"
	VerbPressKeys Verb = "press_keys"
	// VerbEnterCredential names the verb, not a secret: the credential's value
	// never appears in a journey document, which is the whole point of the verb.
	VerbEnterCredential Verb = "enter_credential" //nolint:gosec // a verb name

	// Perception.
	VerbObserve Verb = "observe"
	VerbRead    Verb = "read"
	VerbCapture Verb = "capture"

	// Synchronisation.
	VerbPause Verb = "pause"
)

The verbs, grouped as the taxonomy groups them.

func (Verb) Known added in v1.3.0

func (v Verb) Known() bool

Known reports whether the verb is one this build compiles.

type Wait added in v1.3.0

type Wait struct {
	// Timeout is the budget in seconds. Zero takes DefaultWaitTimeout.
	Timeout float64 `json:"timeout,omitempty"`
	// Interval is the poll period in seconds. Zero takes DefaultWaitInterval.
	Interval float64 `json:"interval,omitempty"`
}

Wait is the poll budget for an assertion.

func (*Wait) Resolved added in v1.3.0

func (w *Wait) Resolved() Wait

Resolved returns the wait with its defaults applied.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL