Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/replay-logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,33 @@ using standard CSV quoting. For spreadsheet safety, CSV cells whose text starts
with `=`, `+`, `-`, or `@`, including after whitespace or control characters,
are prefixed with an apostrophe. This changes only the presentation cell;
`event_json` retains the original event values for lossless import and analysis.

## Verify recorded command transitions

Use offline verification to check whether each logged command's immediate
controller input state matches its recorded `stateAfter`:

```bash
opencontroller replay verify ./replays/session-001/events.jsonl
```

For each command event, the verifier applies the command to that event's
`stateBefore` and compares `buttons`, `analogButtons`, `triggers`, `sticks`,
`dpad`, `touchpad`, and `motion` with `stateAfter`. It checks events
independently, so the log does not need to contain a complete state chain.
Timed commands are checked at their logged boundary: a timed press is checked
while pressed, and its later logged release is checked as a separate event.
Verification does not wait for `durationMs`, synthesize a release, open an
adapter, or send input to a host.

The output reports `timestamp`, controller and session identity, connection
state, `updatedAt`, `status`, and `feedback` as context that it cannot verify.
Legacy events missing a profile, state snapshot, or command data and commands
that cannot be simulated under a supported profile are reported as
unverifiable with their one-based event and line numbers. A mismatch reports
the first differing deterministic field path. Malformed JSON or event records
are errors with their line and event location.

This verifies OpenController's recorded state semantics only. It cannot
confirm adapter handoff, whether a host received a command, or any GUI or game
behavior or outcome.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"type":"command","timestamp":1,"controllerId":"pad-1","profile":"xbox","command":{"type":"press","button":"A"}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type":"annotation","timestamp":10,"label":"valid preceding event"}
{"type":"state","timestamp":11}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
not-json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"type":"command","timestamp":1,"controllerId":"pad-1","profile":"xbox","command":{"type":"press","button":"A","durationMs":60000},"stateBefore":{"id":"pad-1","profile":"xbox","connected":false,"buttons":{"A":false,"B":false,"X":false,"Y":false,"LB":false,"RB":false,"LT":false,"RT":false,"BACK":false,"START":false,"GUIDE":false,"LS":false,"RS":false,"DPAD_UP":false,"DPAD_DOWN":false,"DPAD_LEFT":false,"DPAD_RIGHT":false},"analogButtons":{"LT":0,"RT":0},"sticks":{"left":{"x":0,"y":0},"right":{"x":0,"y":0}},"dpad":{"up":false,"down":false,"left":false,"right":false},"touchpad":{"pressed":false,"contacts":[]},"motion":{"acceleration":{"x":0,"y":0,"z":0},"gyroscope":{"x":0,"y":0,"z":0},"orientation":{"x":0,"y":0,"z":0}},"status":{"battery":{"level":1,"charging":false,"wired":true,"low":false},"connection":{"quality":1,"latencyMs":0,"packetLoss":0}},"feedback":{"rumble":{"weakMotor":0,"strongMotor":0,"leftTriggerMotor":0,"rightTriggerMotor":0},"lights":{"red":0,"green":0,"blue":0,"brightness":0,"playerIndex":0,"playerLightMask":0}},"updatedAt":1791160985387},"stateAfter":{"id":"pad-1","profile":"xbox","connected":false,"buttons":{"A":false,"B":false,"X":false,"Y":false,"LB":false,"RB":false,"LT":false,"RT":false,"BACK":false,"START":false,"GUIDE":false,"LS":false,"RS":false,"DPAD_UP":false,"DPAD_DOWN":false,"DPAD_LEFT":false,"DPAD_RIGHT":false},"analogButtons":{"LT":0,"RT":0},"sticks":{"left":{"x":0,"y":0},"right":{"x":0,"y":0}},"dpad":{"up":false,"down":false,"left":false,"right":false},"touchpad":{"pressed":false,"contacts":[]},"motion":{"acceleration":{"x":0,"y":0,"z":0},"gyroscope":{"x":0,"y":0,"z":0},"orientation":{"x":0,"y":0,"z":0}},"status":{"battery":{"level":1,"charging":false,"wired":true,"low":false},"connection":{"quality":1,"latencyMs":0,"packetLoss":0}},"feedback":{"rumble":{"weakMotor":0,"strongMotor":0,"leftTriggerMotor":0,"rightTriggerMotor":0},"lights":{"red":0,"green":0,"blue":0,"brightness":0,"playerIndex":0,"playerLightMask":0}},"updatedAt":999}}
2 changes: 2 additions & 0 deletions packages/cli/src/__tests__/fixtures/replay-verify-timed.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type":"command","timestamp":1,"controllerId":"pad-1","profile":"xbox","command":{"type":"press","button":"A","durationMs":60000},"stateBefore":{"id":"pad-1","profile":"xbox","connected":false,"buttons":{"A":false,"B":false,"X":false,"Y":false,"LB":false,"RB":false,"LT":false,"RT":false,"BACK":false,"START":false,"GUIDE":false,"LS":false,"RS":false,"DPAD_UP":false,"DPAD_DOWN":false,"DPAD_LEFT":false,"DPAD_RIGHT":false},"analogButtons":{"LT":0,"RT":0},"sticks":{"left":{"x":0,"y":0},"right":{"x":0,"y":0}},"dpad":{"up":false,"down":false,"left":false,"right":false},"touchpad":{"pressed":false,"contacts":[]},"motion":{"acceleration":{"x":0,"y":0,"z":0},"gyroscope":{"x":0,"y":0,"z":0},"orientation":{"x":0,"y":0,"z":0}},"status":{"battery":{"level":1,"charging":false,"wired":true,"low":false},"connection":{"quality":1,"latencyMs":0,"packetLoss":0}},"feedback":{"rumble":{"weakMotor":0,"strongMotor":0,"leftTriggerMotor":0,"rightTriggerMotor":0},"lights":{"red":0,"green":0,"blue":0,"brightness":0,"playerIndex":0,"playerLightMask":0}},"updatedAt":1791160985387},"stateAfter":{"id":"context-id-0","profile":"xbox","connected":true,"buttons":{"A":true,"B":false,"X":false,"Y":false,"LB":false,"RB":false,"LT":false,"RT":false,"BACK":false,"START":false,"GUIDE":false,"LS":false,"RS":false,"DPAD_UP":false,"DPAD_DOWN":false,"DPAD_LEFT":false,"DPAD_RIGHT":false},"analogButtons":{"LT":0,"RT":0},"sticks":{"left":{"x":0,"y":0},"right":{"x":0,"y":0}},"dpad":{"up":false,"down":false,"left":false,"right":false},"touchpad":{"pressed":false,"contacts":[]},"motion":{"acceleration":{"x":0,"y":0,"z":0},"gyroscope":{"x":0,"y":0,"z":0},"orientation":{"x":0,"y":0,"z":0}},"status":{"hostStatus":"context-0"},"feedback":{"rumble":"context-0"},"updatedAt":999}}
{"type":"command","timestamp":60002,"controllerId":"pad-1","profile":"xbox","command":{"type":"release","button":"A"},"stateBefore":{"id":"pad-1","profile":"xbox","connected":false,"buttons":{"A":true,"B":false,"X":false,"Y":false,"LB":false,"RB":false,"LT":false,"RT":false,"BACK":false,"START":false,"GUIDE":false,"LS":false,"RS":false,"DPAD_UP":false,"DPAD_DOWN":false,"DPAD_LEFT":false,"DPAD_RIGHT":false},"analogButtons":{"LT":0,"RT":0},"sticks":{"left":{"x":0,"y":0},"right":{"x":0,"y":0}},"dpad":{"up":false,"down":false,"left":false,"right":false},"touchpad":{"pressed":false,"contacts":[]},"motion":{"acceleration":{"x":0,"y":0,"z":0},"gyroscope":{"x":0,"y":0,"z":0},"orientation":{"x":0,"y":0,"z":0}},"status":{"battery":{"level":1,"charging":false,"wired":true,"low":false},"connection":{"quality":1,"latencyMs":0,"packetLoss":0}},"feedback":{"rumble":{"weakMotor":0,"strongMotor":0,"leftTriggerMotor":0,"rightTriggerMotor":0},"lights":{"red":0,"green":0,"blue":0,"brightness":0,"playerIndex":0,"playerLightMask":0}},"updatedAt":999},"stateAfter":{"id":"context-id-1","profile":"xbox","connected":true,"buttons":{"A":false,"B":false,"X":false,"Y":false,"LB":false,"RB":false,"LT":false,"RT":false,"BACK":false,"START":false,"GUIDE":false,"LS":false,"RS":false,"DPAD_UP":false,"DPAD_DOWN":false,"DPAD_LEFT":false,"DPAD_RIGHT":false},"analogButtons":{"LT":0,"RT":0},"sticks":{"left":{"x":0,"y":0},"right":{"x":0,"y":0}},"dpad":{"up":false,"down":false,"left":false,"right":false},"touchpad":{"pressed":false,"contacts":[]},"motion":{"acceleration":{"x":0,"y":0,"z":0},"gyroscope":{"x":0,"y":0,"z":0},"orientation":{"x":0,"y":0,"z":0}},"status":{"hostStatus":"context-1"},"feedback":{"rumble":"context-1"},"updatedAt":1000}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"type":"command","timestamp":1,"controllerId":"pad-1","profile":"xbox","command":{"type":"future-command"}}
{"type":"command","timestamp":2,"controllerId":"pad-1","profile":"arcade","command":{"type":"press","button":"A"}}
{"type":"command","timestamp":3,"controllerId":"pad-1","profile":"xbox","command":{"type":"setButton","button":"A"}}
71 changes: 71 additions & 0 deletions packages/cli/src/__tests__/replay-verify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, test } from "bun:test";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { verifyReplay } from "../commands/replay-verify";

const fixtures = join(import.meta.dir, "fixtures");

describe("replay verify", () => {
test("verifies timed press and separate release at their logged boundaries", async () => {
const result = await verifyReplay(
join(fixtures, "replay-verify-timed.jsonl"),
);

expect(result).toEqual({ events: 2, verified: 2, unverifiable: [] });
});

test("reports the first differing control field with event and line", async () => {
const result = await verifyReplay(
join(fixtures, "replay-verify-mismatch.jsonl"),
);

expect(result.mismatch).toEqual({
event: 1,
line: 1,
field: "buttons.A",
});
expect(result.verified).toBe(0);
});

test("reports malformed JSON with one-based line and event", async () => {
const input = join(fixtures, "replay-verify-malformed.jsonl");
const content = await readFile(input, "utf8");
expect(content).toContain("not-json");
await expect(verifyReplay(input)).rejects.toThrow(`at line 1 (event 1)`);
});

test("rejects malformed recognized state events with line and event location", async () => {
const input = join(fixtures, "replay-verify-malformed-state-event.jsonl");

await expect(verifyReplay(input)).rejects.toThrow(
`Invalid state event in ${input} at line 2 (event 2): expected controllerId and state`,
);
});

test("marks legacy commands unverifiable rather than inferring missing state", async () => {
const result = await verifyReplay(
join(fixtures, "replay-verify-legacy.jsonl"),
);

expect(result.verified).toBe(0);
expect(result.unverifiable).toEqual([
{
event: 1,
line: 1,
reason: "legacy command is missing stateBefore",
},
]);
});

test("reports unsupported profiles, command types, and missing command fields", async () => {
const result = await verifyReplay(
join(fixtures, "replay-verify-unsupported.jsonl"),
);

expect(result.unverifiable.map(({ reason }) => reason)).toEqual([
'unsupported command type "future-command"',
'unsupported or missing profile "arcade"',
"command is missing required boolean pressed",
]);
});
});
Loading
Loading