[GH-ISSUE #3391] [Bug] Accepted scenario_start action creates duplicate capture step IDs #1260

Closed
opened 2026-10-16 21:03:29 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3391

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

capture-browser-scenario accepts a single action whose step_id is scenario_start, then emits the same ID for both the initial state and the action. The accepted action completes and its capture reports completeness.status: "complete", equality_eligible: true, and confirmed owned-browser cleanup. Comparing the capture with itself through compare-web-captures returns overall_status: "unknown" with duplicate-step alignment failures.

Changing only the action ID to settle produces step_ids: ["scenario_start", "settle"] and restores an unchanged self-comparison with two aligned steps and no alignment failures. Both captures and both comparisons exited successfully in the observed reproduction.

Reproduction

  1. Prepare a clean source checkout at the tested revision and install its locked dependencies and build the runtime:
git clone https://github.com/morluto/rea.git rea-scenario-repro
cd rea-scenario-repro
git checkout --detach 8dde6370d0809ad04cda3342d4e6f290f48aa063
npm ci
npm run build:cached
  1. Set CHROME to the absolute path of an already-installed Chrome-family executable. Run the following from the checkout root. It creates every input file, serves only a local fixture, invokes the public CLI, closes its server, and removes its disposable output directory outside the checkout. The observed reproduction used an already-built runtime with installed dependencies.
: "${CHROME:?Set CHROME to an installed Chrome-family executable}"
case_dir="$(mktemp -d)"
mkdir -p "$case_dir/home" "$case_dir/temp"
cat > "$case_dir/repro.cjs" <<'JS'
const { createServer } = require('node:http');
const { execFile } = require('node:child_process');
const { promisify } = require('node:util');
const { writeFile } = require('node:fs/promises');
const { join } = require('node:path');
const run = promisify(execFile);
async function cli(args) {
  const result = await run(process.execPath, ['scripts/rea.mjs', ...args, '--json'], {
    cwd: process.cwd(), env: process.env, timeout: 120000, maxBuffer: 1048576
  });
  return JSON.parse(result.stdout);
}
(async () => {
  const server = createServer((request, response) => {
    response.setHeader('Content-Type', 'text/html');
    response.end('<!doctype html><title>Fixture</title>');
  });
  await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
  try {
    const url = `http://127.0.0.1:${server.address().port}/`;
    for (const stepId of ['scenario_start', 'settle']) {
      const input = join(__dirname, `${stepId}-input.json`);
      const capturePath = join(__dirname, `${stepId}-capture.json`);
      await writeFile(input, JSON.stringify({
        browser: { mode: 'launch', executable_path: process.env.CHROME },
        start_url: { url },
        actions: [{ step_id: stepId, action: 'wait_for_timeout', duration_ms: 1 }]
      }));
      const capture = (await cli(['capture-browser-scenario', input])).normalized_result;
      await writeFile(capturePath, JSON.stringify(capture));
      const comparison = (await cli(['compare-web-captures', capturePath, capturePath])).normalized_result;
      console.log(JSON.stringify({
        action_id: stepId,
        step_ids: capture.steps.map(step => step.step_id),
        capture_completeness: capture.completeness,
        cleanup: capture.browser.cleanup,
        overall_status: comparison.overall_status,
        alignment: comparison.alignment
      }, null, 2));
    }
  } finally {
    await new Promise(resolve => server.close(resolve));
  }
})().catch(error => { console.error(error); process.exitCode = 1; });
JS
env HOME="$case_dir/home" TMPDIR="$case_dir/temp" node "$case_dir/repro.cjs"
rm -rf "$case_dir"
  1. Inspect the printed step IDs, completeness, cleanup, comparison status, and alignment. The scenario_start case emits duplicate step IDs and fails self-alignment; the settle control emits two distinct step IDs and compares unchanged.

Expected behavior

A scenario with one otherwise valid, unique action ID should produce an initial state and one independently identified action record. A complete successful capture should align with itself. If an ID is reserved for the initial state, validation must identify that restriction before running the scenario instead of accepting it and producing an ambiguous capture.

docs/browser-scenario-contract.md:177 specifies an initial state followed by one record per declared action, and docs/browser-scenario-contract.md:306 specifies exact, unique step-ID alignment for complete scenario captures. src/domain/browserScenario.ts:108 validates uniqueness among caller action IDs without reserving the initial-state ID.

Impact and scope

Impact level: P2. The assessment is supported by the observed reproduction: a supported caller action ID creates ambiguous step Evidence and prevents a complete successful scenario from being compared even with itself. The failure is limited to IDs colliding with the generated initial-state record.

Workaround status: available. Renaming the action to settle was executed with the same fixture and restored complete self-alignment and an unchanged result.

REA version

6.4.0 (main at 8dde6370d0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64; locked playwright-core 1.58.2; Google Chrome 155.0.8059.40. The runtime was already built. The observed reproduction used owned headless browser profiles and local HTTP pages.

Target and artifact context

A local HTTP fixture serving <!doctype html><title>Fixture</title>, a provider-owned launched Chrome-family browser, and a one-action scenario using wait_for_timeout with duration_ms: 1. The script retains the normalized capture as JSON and supplies that same capture file as both inputs to compare-web-captures. All generated files stay in a disposable directory outside the checkout until the script removes it.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

The accepted action completed, and the capture contained step_ids: ["scenario_start", "scenario_start"], completeness.status: "complete", equality_eligible: true, and confirmed owned-browser cleanup. Its observed self-comparison returned:

{
  "overall_status": "unknown",
  "alignment": {
    "status": "failed",
    "aligned_steps": 0,
    "before_only": [],
    "after_only": [],
    "failures": [
      {
        "code": "duplicate_before_step_id",
        "step_id": "scenario_start",
        "reason": "before capture contains duplicate step_id scenario_start."
      },
      {
        "code": "duplicate_after_step_id",
        "step_id": "scenario_start",
        "reason": "after capture contains duplicate step_id scenario_start."
      }
    ]
  }
}

The control changed only the action ID to settle. It returned step_ids: ["scenario_start", "settle"] and an unchanged self-comparison with two aligned steps and no alignment failures. Both captures and both comparisons exited successfully.

Typed outcome or recovery expected

Repeat the reproduction. A successfully accepted scenario must not produce duplicate step IDs or fail self-alignment because of its generated initial-state ID. If scenario_start becomes invalid input, the rejection must occur before browser actions and explain the ID restriction. The settle control must continue to capture two records and compare as unchanged.

Analysis or suspected cause

Source-derived reasoning: src/domain/browserScenario.ts:108 checks uniqueness only among caller action IDs, while src/browser/PlaywrightBrowserScenarioProvider.ts:130 emits scenario_start for the initial state independently of those caller IDs. This permits the generated initial state and the accepted action to share the same ID, contrary to the exact, unique alignment contract at docs/browser-scenario-contract.md:306. The executed CLI capture and self-comparison establish the failure; the source inspection explains the collision.

Additional context

Related open issue #3173 concerns missing network-content recovery guidance in comparisons with unique, aligned steps; this report concerns duplicate step IDs generated during capture.

Related closed issue #2169 concerns false equality under mismatched event-family coverage; this report uses a complete capture compared with itself and fails step alignment.

Related completed issue #378 defines step alignment and normalization; it does not report accepted action IDs colliding with the generated initial-state record.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3391 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior `capture-browser-scenario` accepts a single action whose `step_id` is `scenario_start`, then emits the same ID for both the initial state and the action. The accepted action completes and its capture reports `completeness.status: "complete"`, `equality_eligible: true`, and confirmed owned-browser cleanup. Comparing the capture with itself through `compare-web-captures` returns `overall_status: "unknown"` with duplicate-step alignment failures. Changing only the action ID to `settle` produces `step_ids: ["scenario_start", "settle"]` and restores an `unchanged` self-comparison with two aligned steps and no alignment failures. Both captures and both comparisons exited successfully in the observed reproduction. ### Reproduction 1. Prepare a clean source checkout at the tested revision and install its locked dependencies and build the runtime: ```sh git clone https://github.com/morluto/rea.git rea-scenario-repro cd rea-scenario-repro git checkout --detach 8dde6370d0809ad04cda3342d4e6f290f48aa063 npm ci npm run build:cached ``` 2. Set `CHROME` to the absolute path of an already-installed Chrome-family executable. Run the following from the checkout root. It creates every input file, serves only a local fixture, invokes the public CLI, closes its server, and removes its disposable output directory outside the checkout. The observed reproduction used an already-built runtime with installed dependencies. ```bash : "${CHROME:?Set CHROME to an installed Chrome-family executable}" case_dir="$(mktemp -d)" mkdir -p "$case_dir/home" "$case_dir/temp" cat > "$case_dir/repro.cjs" <<'JS' const { createServer } = require('node:http'); const { execFile } = require('node:child_process'); const { promisify } = require('node:util'); const { writeFile } = require('node:fs/promises'); const { join } = require('node:path'); const run = promisify(execFile); async function cli(args) { const result = await run(process.execPath, ['scripts/rea.mjs', ...args, '--json'], { cwd: process.cwd(), env: process.env, timeout: 120000, maxBuffer: 1048576 }); return JSON.parse(result.stdout); } (async () => { const server = createServer((request, response) => { response.setHeader('Content-Type', 'text/html'); response.end('<!doctype html><title>Fixture</title>'); }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); try { const url = `http://127.0.0.1:${server.address().port}/`; for (const stepId of ['scenario_start', 'settle']) { const input = join(__dirname, `${stepId}-input.json`); const capturePath = join(__dirname, `${stepId}-capture.json`); await writeFile(input, JSON.stringify({ browser: { mode: 'launch', executable_path: process.env.CHROME }, start_url: { url }, actions: [{ step_id: stepId, action: 'wait_for_timeout', duration_ms: 1 }] })); const capture = (await cli(['capture-browser-scenario', input])).normalized_result; await writeFile(capturePath, JSON.stringify(capture)); const comparison = (await cli(['compare-web-captures', capturePath, capturePath])).normalized_result; console.log(JSON.stringify({ action_id: stepId, step_ids: capture.steps.map(step => step.step_id), capture_completeness: capture.completeness, cleanup: capture.browser.cleanup, overall_status: comparison.overall_status, alignment: comparison.alignment }, null, 2)); } } finally { await new Promise(resolve => server.close(resolve)); } })().catch(error => { console.error(error); process.exitCode = 1; }); JS env HOME="$case_dir/home" TMPDIR="$case_dir/temp" node "$case_dir/repro.cjs" rm -rf "$case_dir" ``` 3. Inspect the printed step IDs, completeness, cleanup, comparison status, and alignment. The `scenario_start` case emits duplicate step IDs and fails self-alignment; the `settle` control emits two distinct step IDs and compares unchanged. ### Expected behavior A scenario with one otherwise valid, unique action ID should produce an initial state and one independently identified action record. A complete successful capture should align with itself. If an ID is reserved for the initial state, validation must identify that restriction before running the scenario instead of accepting it and producing an ambiguous capture. `docs/browser-scenario-contract.md:177` specifies an initial state followed by one record per declared action, and `docs/browser-scenario-contract.md:306` specifies exact, unique step-ID alignment for complete scenario captures. `src/domain/browserScenario.ts:108` validates uniqueness among caller action IDs without reserving the initial-state ID. ### Impact and scope Impact level: P2. The assessment is supported by the observed reproduction: a supported caller action ID creates ambiguous step Evidence and prevents a complete successful scenario from being compared even with itself. The failure is limited to IDs colliding with the generated initial-state record. Workaround status: available. Renaming the action to `settle` was executed with the same fixture and restored complete self-alignment and an unchanged result. ### REA version 6.4.0 (main at 8dde6370d0809ad04cda3342d4e6f290f48aa063) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64; locked playwright-core 1.58.2; Google Chrome 155.0.8059.40. The runtime was already built. The observed reproduction used owned headless browser profiles and local HTTP pages. ### Target and artifact context A local HTTP fixture serving `<!doctype html><title>Fixture</title>`, a provider-owned launched Chrome-family browser, and a one-action scenario using `wait_for_timeout` with `duration_ms: 1`. The script retains the normalized capture as JSON and supplies that same capture file as both inputs to `compare-web-captures`. All generated files stay in a disposable directory outside the checkout until the script removes it. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence The accepted action completed, and the capture contained `step_ids: ["scenario_start", "scenario_start"]`, `completeness.status: "complete"`, `equality_eligible: true`, and confirmed owned-browser cleanup. Its observed self-comparison returned: ```text { "overall_status": "unknown", "alignment": { "status": "failed", "aligned_steps": 0, "before_only": [], "after_only": [], "failures": [ { "code": "duplicate_before_step_id", "step_id": "scenario_start", "reason": "before capture contains duplicate step_id scenario_start." }, { "code": "duplicate_after_step_id", "step_id": "scenario_start", "reason": "after capture contains duplicate step_id scenario_start." } ] } } ``` The control changed only the action ID to `settle`. It returned `step_ids: ["scenario_start", "settle"]` and an `unchanged` self-comparison with two aligned steps and no alignment failures. Both captures and both comparisons exited successfully. ### Typed outcome or recovery expected Repeat the reproduction. A successfully accepted scenario must not produce duplicate step IDs or fail self-alignment because of its generated initial-state ID. If `scenario_start` becomes invalid input, the rejection must occur before browser actions and explain the ID restriction. The `settle` control must continue to capture two records and compare as `unchanged`. ### Analysis or suspected cause Source-derived reasoning: `src/domain/browserScenario.ts:108` checks uniqueness only among caller action IDs, while `src/browser/PlaywrightBrowserScenarioProvider.ts:130` emits `scenario_start` for the initial state independently of those caller IDs. This permits the generated initial state and the accepted action to share the same ID, contrary to the exact, unique alignment contract at `docs/browser-scenario-contract.md:306`. The executed CLI capture and self-comparison establish the failure; the source inspection explains the collision. ### Additional context Related open issue [#3173](https://github.com/morluto/rea/issues/3173) concerns missing network-content recovery guidance in comparisons with unique, aligned steps; this report concerns duplicate step IDs generated during capture. Related closed issue [#2169](https://github.com/morluto/rea/issues/2169) concerns false equality under mismatched event-family coverage; this report uses a complete capture compared with itself and fails step alignment. Related completed issue [#378](https://github.com/morluto/rea/issues/378) defines step alignment and normalization; it does not report accepted action IDs colliding with the generated initial-state record.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1260
No description provided.