[GH-ISSUE #3987] [Bug] An unrelated empty template key erases a literal WebMCP tool name #1583

Closed
opened 2026-10-16 21:07:26 +00:00 by bsakaguchi · 2 comments
Owner

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

Before submitting

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

Area

Evidence, artifacts, or reconstruction

Observed behavior

The public analyze-web-bundle command reports an explicit WebMCP tool name as null when an unrelated later property has an empty computed template key. Equivalent ordinary and computed empty-string keys retain the name. This loses a known tool identity in the static declaration inventory.

Observed through five successful CLI calls, one per source variant, with empty stderr. The empty template case returned one declaration with name: null. The ordinary empty-string, computed empty-string, and nonempty-template cases each returned one declaration with name: "catalog". The unresolved-key control returned one declaration with name: null.

{"label":"template-empty","name":null}
{"label":"ordinary-empty","name":"catalog"}
{"label":"computed-string-empty","name":"catalog"}
{"label":"template-nonempty","name":"catalog"}
{"label":"unknown-key","name":null}

These are selected fields from the recorded CLI results. The temporary test passed because it asserted these baseline observations. The failure is the incorrect metadata in the first result. Real Chrome behavior and MCP transport were not exercised.

Reproduction

Use the baseline under Runtime or environment with its dependencies installed and compiled CLI available. The reproduction uses the repository's fake CDP browser over loopback HTTP/WebSocket, so it needs no real Chrome instance or external service.

  1. Create a temporary acceptance test at tests/acceptance/browser/verifyWebmcpEmptyProperty.test.ts with the following contents.
import { expect, it } from "vitest";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";

const run = promisify(execFile);
it("records CLI name extraction for exact and unresolved property keys", async () => {
  const home = await createTestTempDirectory("webmcp-property-home-");
  let source = "";
  const browser = await startFakeCdpBrowser({
    commandResult: (command) => command.method === "Debugger.getScriptSource"
      ? { scriptSource: source } : undefined,
  });
  try {
    const cases = [
      ["template-empty", 'document.modelContext.registerTool({name:"catalog", [``]:0});', null],
      ["ordinary-empty", 'document.modelContext.registerTool({name:"catalog", "":0});', "catalog"],
      ["computed-string-empty", 'document.modelContext.registerTool({name:"catalog", [""]:0});', "catalog"],
      ["template-nonempty", 'document.modelContext.registerTool({name:"catalog", [`other`]:0});', "catalog"],
      ["unknown-key", 'document.modelContext.registerTool({name:"catalog", [unknownKey]:0});', null],
    ] as const;
    for (const [label, text, expectedObserved] of cases) {
      source = text;
      const { stdout, stderr } = await run(process.execPath, [
        "scripts/rea.mjs", "analyze-web-bundle", browser.endpoint,
        "allowed-page", "--observation-ms", "0", "--json",
      ], { env: { ...process.env, HOME: home } });
      const result = JSON.parse(stdout).normalized_result;
      const declarations = result.observations.webmcp_declarations;
      console.log(JSON.stringify({label, source, declarations, coverage: result.coverage, stderr}));
      expect(declarations.length).toBeGreaterThan(0);
      expect(declarations.every((item: {name: string | null}) => item.name === expectedObserved)).toBe(true);
    }
  } finally { await browser.close(); }
}, 60_000);
  1. Run npx --no-install vitest run tests/acceptance/browser/verifyWebmcpEmptyProperty.test.ts with the repository's pinned Node.js toolchain. The test starts the fake browser, supplies each source string to Debugger.getScriptSource, and runs the compiled CLI once per input. Each CLI call uses a disposable home directory. The assertions record the current baseline results, including the defect.
  2. Inspect the logged declarations for each case, then delete the temporary acceptance test.

Expected behavior

Static WebMCP metadata should preserve the literal name "catalog" when an unrelated property has an exactly known empty-string key. An empty template literal without substitutions has the same property key as an empty string literal. Neither key can overwrite name. Genuinely unresolved later keys should continue to leave the name unknown because they may overwrite it.

Impact and scope

A valid, bounded syntax case loses explicit identity in the returned static WebMCP inventory. The differential controls establish a consequential metadata defect, with no observed crash, execution, or security-boundary crossing. Frequency outside these inputs is unknown.

Workaround status: available. The literal name remains directly readable in the captured source used by the probe. An analyst can recover it manually without changing the application. Equivalent empty-string spellings also preserve the automated result, but editing the target is not required for manual recovery.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Static JavaScript declarations supplied through the repository fake CDP provider; no real Chrome or page-tool execution.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: AGENTS.md:13 requires preservation of source metadata and AGENTS.md:19 requires consumers to preserve identity using the owner's parsed facts; docs/browser-observation.md:19 and src/contracts/browserToolContracts.ts:174 document static WebMCP declaration analysis. The existing overwrite-order control at src/domain/webBundleAnalyzer.test.ts:992 preserves names for an unrelated computed empty-string key, while src/domain/javascript/javascriptAstValues.ts:77 distinguishes exact empty template keys from unresolved keys.
  • Failure source: The executed compiled CLI returned the five results above through tests/fixtures/fakeCdpBrowser.ts:22. Source-derived explanation: src/domain/webBundleAnalyzerAst.ts:21 clears a prior value when a decoded key is empty and its syntax is not a string literal; src/domain/webBundleAnalyzerInspection.ts:296 uses this lookup for the declaration name and projects the missing value to null.

The CLI execution establishes the failure through the supported capture and analysis workflow. The source explanation is corroborating reasoning, not a claim that source inspection alone reproduced it. The analyzed JavaScript was not executed.

Typed outcome or recovery expected

Keep name: "catalog" for the exactly known empty template key, while a genuinely unresolved later key must still leave the name unknown.

Analysis or suspected cause

Source-derived: src/domain/webBundleAnalyzerAst.ts:21-24 clears the prior value when the decoded key is empty unless the syntax is StringLiteral. src/domain/webBundleAnalyzerInspection.ts:296 reads this value and projects it to null.

Additional context

Freshness check: the cited failing implementation is unchanged on origin/main at fc712b3750. The only change to a cited reproduction entry point normalizes global CLI flag syntax. The baseline reproduction was not rerun on that revision.

Restoration check: Run the same CLI probe. The empty template case must return name: "catalog", matching all three exact unrelated-key controls. The unresolved-key control must still return name: null. Successful restoration must preserve the distinction between an exactly known empty key and a key whose value is unknown.

Related #3331 and PR #3765 concern schema keywords being reported as input names when literal properties are absent. This report supplies literal properties or an explicit name. PR #3480 changes Electron option/callee consumers, and PR #2966 changes primitive key decoding; neither changes the WebMCP consumer responsible for this behavior.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3987 ### 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 Evidence, artifacts, or reconstruction ### Observed behavior The public `analyze-web-bundle` command reports an explicit WebMCP tool name as `null` when an unrelated later property has an empty computed template key. Equivalent ordinary and computed empty-string keys retain the name. This loses a known tool identity in the static declaration inventory. Observed through five successful CLI calls, one per source variant, with empty stderr. The empty template case returned one declaration with `name: null`. The ordinary empty-string, computed empty-string, and nonempty-template cases each returned one declaration with `name: "catalog"`. The unresolved-key control returned one declaration with `name: null`. ```json {"label":"template-empty","name":null} {"label":"ordinary-empty","name":"catalog"} {"label":"computed-string-empty","name":"catalog"} {"label":"template-nonempty","name":"catalog"} {"label":"unknown-key","name":null} ``` These are selected fields from the recorded CLI results. The temporary test passed because it asserted these baseline observations. The failure is the incorrect metadata in the first result. Real Chrome behavior and MCP transport were not exercised. ### Reproduction Use the baseline under Runtime or environment with its dependencies installed and compiled CLI available. The reproduction uses the repository's fake CDP browser over loopback HTTP/WebSocket, so it needs no real Chrome instance or external service. 1. Create a temporary acceptance test at `tests/acceptance/browser/verifyWebmcpEmptyProperty.test.ts` with the following contents. ```ts import { expect, it } from "vitest"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const run = promisify(execFile); it("records CLI name extraction for exact and unresolved property keys", async () => { const home = await createTestTempDirectory("webmcp-property-home-"); let source = ""; const browser = await startFakeCdpBrowser({ commandResult: (command) => command.method === "Debugger.getScriptSource" ? { scriptSource: source } : undefined, }); try { const cases = [ ["template-empty", 'document.modelContext.registerTool({name:"catalog", [``]:0});', null], ["ordinary-empty", 'document.modelContext.registerTool({name:"catalog", "":0});', "catalog"], ["computed-string-empty", 'document.modelContext.registerTool({name:"catalog", [""]:0});', "catalog"], ["template-nonempty", 'document.modelContext.registerTool({name:"catalog", [`other`]:0});', "catalog"], ["unknown-key", 'document.modelContext.registerTool({name:"catalog", [unknownKey]:0});', null], ] as const; for (const [label, text, expectedObserved] of cases) { source = text; const { stdout, stderr } = await run(process.execPath, [ "scripts/rea.mjs", "analyze-web-bundle", browser.endpoint, "allowed-page", "--observation-ms", "0", "--json", ], { env: { ...process.env, HOME: home } }); const result = JSON.parse(stdout).normalized_result; const declarations = result.observations.webmcp_declarations; console.log(JSON.stringify({label, source, declarations, coverage: result.coverage, stderr})); expect(declarations.length).toBeGreaterThan(0); expect(declarations.every((item: {name: string | null}) => item.name === expectedObserved)).toBe(true); } } finally { await browser.close(); } }, 60_000); ``` 2. Run `npx --no-install vitest run tests/acceptance/browser/verifyWebmcpEmptyProperty.test.ts` with the repository's pinned Node.js toolchain. The test starts the fake browser, supplies each source string to `Debugger.getScriptSource`, and runs the compiled CLI once per input. Each CLI call uses a disposable home directory. The assertions record the current baseline results, including the defect. 3. Inspect the logged `declarations` for each case, then delete the temporary acceptance test. ### Expected behavior Static WebMCP metadata should preserve the literal name `"catalog"` when an unrelated property has an exactly known empty-string key. An empty template literal without substitutions has the same property key as an empty string literal. Neither key can overwrite `name`. Genuinely unresolved later keys should continue to leave the name unknown because they may overwrite it. ### Impact and scope A valid, bounded syntax case loses explicit identity in the returned static WebMCP inventory. The differential controls establish a consequential metadata defect, with no observed crash, execution, or security-boundary crossing. Frequency outside these inputs is unknown. Workaround status: available. The literal name remains directly readable in the captured source used by the probe. An analyst can recover it manually without changing the application. Equivalent empty-string spellings also preserve the automated result, but editing the target is not required for manual recovery. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Static JavaScript declarations supplied through the repository fake CDP provider; no real Chrome or page-tool execution. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence - Expected source: `AGENTS.md:13` requires preservation of source metadata and `AGENTS.md:19` requires consumers to preserve identity using the owner's parsed facts; `docs/browser-observation.md:19` and `src/contracts/browserToolContracts.ts:174` document static WebMCP declaration analysis. The existing overwrite-order control at `src/domain/webBundleAnalyzer.test.ts:992` preserves names for an unrelated computed empty-string key, while `src/domain/javascript/javascriptAstValues.ts:77` distinguishes exact empty template keys from unresolved keys. - Failure source: The executed compiled CLI returned the five results above through `tests/fixtures/fakeCdpBrowser.ts:22`. Source-derived explanation: `src/domain/webBundleAnalyzerAst.ts:21` clears a prior value when a decoded key is empty and its syntax is not a string literal; `src/domain/webBundleAnalyzerInspection.ts:296` uses this lookup for the declaration name and projects the missing value to `null`. The CLI execution establishes the failure through the supported capture and analysis workflow. The source explanation is corroborating reasoning, not a claim that source inspection alone reproduced it. The analyzed JavaScript was not executed. ### Typed outcome or recovery expected Keep name: "catalog" for the exactly known empty template key, while a genuinely unresolved later key must still leave the name unknown. ### Analysis or suspected cause Source-derived: src/domain/webBundleAnalyzerAst.ts:21-24 clears the prior value when the decoded key is empty unless the syntax is StringLiteral. src/domain/webBundleAnalyzerInspection.ts:296 reads this value and projects it to null. ### Additional context Freshness check: the cited failing implementation is unchanged on origin/main at fc712b3750cbe16755c4f78c1f90fdf09fbaca31. The only change to a cited reproduction entry point normalizes global CLI flag syntax. The baseline reproduction was not rerun on that revision. Restoration check: Run the same CLI probe. The empty template case must return `name: "catalog"`, matching all three exact unrelated-key controls. The unresolved-key control must still return `name: null`. Successful restoration must preserve the distinction between an exactly known empty key and a key whose value is unknown. Related #3331 and PR #3765 concern schema keywords being reported as input names when literal properties are absent. This report supplies literal properties or an explicit name. PR #3480 changes Electron option/callee consumers, and PR #2966 changes primitive key decoding; neither changes the WebMCP consumer responsible for this behavior.
Author
Owner

@XinwangTao commented on GitHub (Oct 15, 2026):

Confirmed — the differential controls make this precise: an empty template literal has the same property key as an empty string literal (no substitutions), so erasing the prior name only for the template spelling is inconsistent with the ordinary/computed empty-string cases that keep "catalog". The AST layer is the right fix point: webBundleAnalyzerAst.ts:21-24 should treat a template literal with no substitutions as an exactly known empty key (same bucket as StringLiteral), while the unresolved-key path ([unknownKey]) keeps clearing the value — that preserves your "genuinely unresolved later keys may overwrite" semantic without dropping a known identity. The existing overwrite-order control at webBundleAnalyzer.test.ts:992 already covers the empty-string spellings; extending it to the template-empty case would lock the expected behavior in.

<!-- gh-comment-id:6188989476 --> @XinwangTao commented on GitHub (Oct 15, 2026): Confirmed — the differential controls make this precise: an empty template literal has the same property key as an empty string literal (no substitutions), so erasing the prior name only for the template spelling is inconsistent with the ordinary/computed empty-string cases that keep "catalog". The AST layer is the right fix point: webBundleAnalyzerAst.ts:21-24 should treat a template literal with no substitutions as an exactly known empty key (same bucket as StringLiteral), while the unresolved-key path ([unknownKey]) keeps clearing the value — that preserves your "genuinely unresolved later keys may overwrite" semantic without dropping a known identity. The existing overwrite-order control at webBundleAnalyzer.test.ts:992 already covers the empty-string spellings; extending it to the template-empty case would lock the expected behavior in.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Empty template, ordinary empty string, computed empty string and nonempty template unrelated keys preserve catalog; unknownKey stays undefined. semanticStaticPropertyKey distinguishes legal empty keys from null/unknown; staticObjectProperty skips exact unrelated keys and stops for unknown overrides. Dedicated regression asserts the five report controls.

Implementation: javascriptAstValues.ts, javascriptAstValues.ts.

The relevant regression checks in webBundleAnalyzerAst.test.ts, webBundleAnalyzer.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380730355 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Empty template, ordinary empty string, computed empty string and nonempty template unrelated keys preserve catalog; unknownKey stays undefined. semanticStaticPropertyKey distinguishes legal empty keys from null/unknown; staticObjectProperty skips exact unrelated keys and stops for unknown overrides. Dedicated regression asserts the five report controls. Implementation: [javascriptAstValues.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/javascriptAstValues.ts#L100), [javascriptAstValues.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/javascriptAstValues.ts#L241). The relevant regression checks in [webBundleAnalyzerAst.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/webBundleAnalyzerAst.test.ts), [webBundleAnalyzer.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/webBundleAnalyzer.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
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#1583
No description provided.