Skip to content

feat: add SceneGraph assertion commands#2

Closed
altaywtf wants to merge 4 commits into
mainfrom
feat/scenegraph-assertions
Closed

feat: add SceneGraph assertion commands#2
altaywtf wants to merge 4 commits into
mainfrom
feat/scenegraph-assertions

Conversation

@altaywtf
Copy link
Copy Markdown
Member

@altaywtf altaywtf commented May 15, 2026

Summary

Adds generic SceneGraph and remote-control runtime primitives to rokit, then rounds out the repo docs, delivery contract, and agent-readiness setup so app repos can build proof loops without copying Roku XML parsing/assertion helpers.

Changed

  • Adds rokit sgnodes for raw SceneGraph output
  • Adds rokit assert-node for one-shot named-node checks
  • Adds rokit wait-node for polling named-node conditions
  • Adds rokit wait-active for foreground app waits
  • Adds rokit press --delay-ms <ms> ... for stable remote navigation sequences
  • Makes active-app parsing tolerate Roku home responses without app attributes
  • Exposes a typed library API from @putdotio/rokit for app-specific scenario scripts
  • Adds docs/DISTRIBUTION.md, docs/READINESS.md, CLAUDE.md -> AGENTS.md, PR/issue templates, Dependabot, and an optional pre-push hook
  • Tightens release CI now that release-bot secrets exist, including no cache in the privileged release setup

Risks

Low-medium. SceneGraph query support depends on the device/app exposing /query/sgnodes/all; app repos should still own product-specific assertions and screenshots.

Verification

  • pnpm verify
  • ROKIT_TARGET=<configured> pnpm live:smoke with command output redirected
  • Docs/readiness checks:
    • top-level docs exist
    • CLAUDE.md is a symlink to AGENTS.md
    • .env.example remains tracked while local env files are ignored
    • git diff --check
  • CI: https://github.com/putdotio/rokit/actions/runs/25928555035

Closes #1 partially; product-specific player scenarios still belong in putio-roku.

Copilot AI review requested due to automatic review settings May 15, 2026 16:08
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds generic SceneGraph query/assert/wait primitives to rokit so downstream app repos can build proof loops against Roku runtime state without copying XML parsing and assertion helpers.

Changes:

  • Add new CLI commands: sgnodes, assert-node, wait-node, and wait-active.
  • Introduce SceneGraph XML helpers (readNamedNodeAttribute, visibility checks, and assertion helpers).
  • Make active-app XML parsing tolerant of <app> nodes with no attributes, with accompanying tests.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test/xml.test.ts Adds coverage for active-app parsing with/without <app> attributes.
test/scenegraph.test.ts Adds unit tests for named-node attribute reads, visibility, and assertion error messages.
test/cli.test.ts Extends CLI help output test to include the new wait-node command.
src/xml.ts Updates readActiveApp regex to handle <app> tags without attributes.
src/scenegraph.ts Adds SceneGraph XML parsing + assertion helpers used by CLI and Roku query wrappers.
src/roku.ts Adds SceneGraph query/wait wrappers and exports waitForActiveApp.
src/cli.ts Wires new CLI commands, argument parsing, and help output for SceneGraph assertions/waits.
README.md Documents new SceneGraph-related CLI commands and their intended boundary.
AGENTS.md Updates project guidance to keep SceneGraph helpers generic and app contracts in app repos.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/scenegraph.ts
return undefined;
}

return readXmlAttribute(attributes, attributeName);
Comment thread src/scenegraph.ts

export const readNamedNodeAttributes = (xml: string, nodeName: string): string | undefined => {
const pattern = new RegExp(
`<[A-Za-z0-9]+\\b(?=[^>]*\\bname="${escapeRegExp(nodeName)}")([^>]*)>`,
Comment thread src/scenegraph.ts
Comment on lines +37 to +41
export const isNamedNodeVisible = (xml: string, nodeName: string): boolean => {
const attributes = readNamedNodeAttributes(xml, nodeName);

return attributes !== undefined && !attributes.includes('visible="false"');
};
Comment thread src/scenegraph.ts
}

if (!attributes) {
throw new Error(`expected SceneGraph node "${nodeName}"`);
Comment thread src/cli.ts
Comment on lines +253 to +269
const parseNodeCondition = (commandName: string, args: readonly string[]): NodeCondition => {
const [nodeName, condition, ...rest] = args;

if (!nodeName || !condition) {
return fail(
`usage: rokit ${commandName} <node-name> <visible|hidden|absent|text|attr> [value] [--timeout-ms <ms>]`,
);
}

if (condition === "visible" || condition === "hidden" || condition === "absent") {
const timeoutMs = parseTimeoutOption(rest, `rokit ${commandName} <node-name> ${condition}`);
return {
expectation: { state: condition },
nodeName,
timeoutMs,
};
}
Comment thread src/cli.ts
Comment on lines +385 to +386
rokit assert-node <node-name> <visible|hidden|absent|text|attr> [value]
rokit wait-node <node-name> <visible|hidden|absent|text|attr> [value] [--timeout-ms <ms>]
Comment thread src/cli.ts
Comment on lines 171 to +229
const parseCommand = (argv: readonly string[]): Command => {
const [name, ...args] = argv;

if (name === "check") {
return { name };
}

if (name === "device-info") {
return { name };
}

if (name === "active-app") {
return { name };
}

if (name === "wait-active") {
const appId = args[0];

if (!appId) {
fail("usage: rokit wait-active <app-id> [--timeout-ms <ms>]");
}

return {
appId,
name,
timeoutMs: parseTimeoutOption(args.slice(1), `rokit ${name} <app-id>`),
};
}

if (name === "launch") {
return { name, args: parseLaunchArgs(args) };
}

if (name === "press") {
if (args.length === 0) {
fail("usage: rokit press <key> [key...]");
}

return { name, keys: args };
}

if (name === "query") {
const path = args[0];

if (!path) {
fail("usage: rokit query <ecp-path>");
}

return { name, path };
}

if (name === "sgnodes") {
return { name };
}

if (name === "assert-node" || name === "wait-node") {
return { name, args: parseNodeCondition(name, args) };
}

Comment thread README.md
Comment on lines +52 to +58

if (!target) {
throw new Error("ROKIT_TARGET is not set");
}

const context: RokuContext = {
target,
@altaywtf
Copy link
Copy Markdown
Member Author

Closing for now; continuing iteration on the branch without PR ceremony.

@altaywtf altaywtf closed this May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extract reusable Roku runtime assertion helpers

2 participants