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
- Variables
- func Compile(j Journey, sessionID string) (plan.Document, error)
- type Assertion
- type ElementFacts
- type Event
- type EventKind
- type Journey
- type NameMatch
- type Occurrence
- type OccurrenceMode
- type Operator
- type Origin
- type OriginKind
- type Scope
- type Selector
- type Step
- type Subject
- type ValueType
- type Verb
- type Wait
Constants ¶
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.
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.
const MaxPauseSeconds = 60.0
MaxPauseSeconds bounds a fixed sleep, matching the Wait tool's clamp.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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" )
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" )
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
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 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.
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" )
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).
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. 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.
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.