[GH-ISSUE #3166] [Bug] Fresh ADB installs with --replace are reported as observed replacements #1104

Closed
opened 2026-10-15 18:31:04 +00:00 by bsakaguchi · 1 comment
Owner

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

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

rea install-adb-package returned normalized_result.replaced: true with confidence: observed for a successful fresh install when the caller supplied --replace. A stateful executable ADB fake independently recorded whether each install was fresh or replaced its existing marker. The same command against the already-installed simulated state returned the same replacement boolean. A fresh install without --replace returned replaced: false.

The call log contained client version discovery and installation commands, with no prior-package-state query. The fresh --replace result included artifact-identity and host-digest limitations, but none explained that replacement was an unobserved request option. These observations establish the result/provenance mismatch through the supported CLI with a simulated ADB client.

Reproduction

  1. Start from a clean checkout at 58300885207133422af235e0f2b10658cf958b03, use the development toolchain pinned by .nvmrc and package.json, and prepare the runtime:

    npm ci
    npm run build:cached
    
  2. Use a new .repro-install directory so the installed marker is initially absent. Create the placeholder file and stateful ADB fake:

    mkdir -p .repro-install/home/config .repro-install/home/cache .repro-install/home/data
    printf 'apk' > .repro-install/app.apk
    cat > .repro-install/adb-stub.sh <<'SH'
    #!/bin/sh
    root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
    printf '%s\n' "$*" >> "$root/adb-calls.txt"
    if [ "$1" = version ]; then
      printf 'Android Debug Bridge version 1.0.41\nVersion 34.0.5-stub\nInstalled as %s\n' "$0"
      exit 0
    fi
    if [ "$1" = -s ] && [ "$3" = install ]; then
      if [ -f "$root/installed" ]; then
        if [ "$4" != -r ]; then
          printf 'Failure [INSTALL_FAILED_ALREADY_EXISTS]\n'
          exit 0
        fi
        printf 'replacement\n' >> "$root/device-events.txt"
      else
        printf 'fresh-install\n' >> "$root/device-events.txt"
      fi
      : > "$root/installed"
      printf 'Performing Streamed Install\nSuccess\n'
      exit 0
    fi
    exit 1
    SH
    chmod +x .repro-install/adb-stub.sh
    
  3. Run the CLI twice with --replace, then remove the fake's installed marker and run a fresh install without --replace. This script gives each CLI child isolated HOME/XDG directories, retains its full JSON, and prints the event log and selected result fields:

    node --input-type=module <<'JS'
    import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';
    import { resolve } from 'node:path';
    import { spawnSync } from 'node:child_process';
    
    const fixtureRoot = resolve('.repro-install');
    const childEnv = {
      ...process.env,
      HOME: resolve(fixtureRoot, 'home'),
      XDG_CONFIG_HOME: resolve(fixtureRoot, 'home/config'),
      XDG_CACHE_HOME: resolve(fixtureRoot, 'home/cache'),
      XDG_DATA_HOME: resolve(fixtureRoot, 'home/data'),
      REA_ADB_PATH: resolve(fixtureRoot, 'adb-stub.sh')
    };
    for (const [name, replace] of [
      ['fresh-replace', true],
      ['existing-replace', true],
      ['fresh-default', false]
    ]) {
      if (name === 'fresh-default') unlinkSync(resolve(fixtureRoot, 'installed'));
      const args = [
        'scripts/rea.mjs', 'install-adb-package', 'emulator-5554',
        resolve(fixtureRoot, 'app.apk'),
        ...(replace ? ['--replace'] : []),
        '--format', 'json', '--full-output'
      ];
      const child = spawnSync(process.execPath, args, { env: childEnv, encoding: 'utf8' });
      if (child.error) throw child.error;
      if (child.status !== 0) throw new Error(`${name}: ${child.stderr || child.stdout}`);
      writeFileSync(resolve(fixtureRoot, `${name}.json`), child.stdout);
    }
    console.log(readFileSync(resolve(fixtureRoot, 'device-events.txt'), 'utf8').trim());
    for (const file of ['fresh-replace', 'existing-replace', 'fresh-default']) {
      const result = JSON.parse(readFileSync(resolve(fixtureRoot, `${file}.json`), 'utf8'));
      console.log(file, JSON.stringify({
        ok: result.ok,
        requested_replace: result.data.parameters.replace,
        replaced: result.data.normalized_result.replaced,
        confidence: result.data.confidence
      }));
    }
    console.log(readFileSync(resolve(fixtureRoot, 'adb-calls.txt'), 'utf8').trim());
    JS
    
  4. Compare the independent event log with the returned fields. The first event is fresh-install, yet the first result reports replaced: true and confidence: observed. The second event is replacement with the same returned fields. The third event is fresh-install with replaced: false.

The fake deliberately returns the same successful ADB install text for fresh and replacement states. The three-byte file is a placeholder accepted by REA's file checks and the fake client, rather than a real APK. No Android device is needed. The verifier executed the CLI calls and controls against a prepared baseline. Dependency installation and build are clean-checkout prerequisites and were not rerun during verification.

Expected behavior

AGENTS.md:5, under Product and Authority, requires observations, derivations, inferences, and unknowns to remain distinct and Evidence provenance to survive both adapters. AGENTS.md:19-21, under Architecture and Cleanup, requires preservation of uncertainty and unresolved applicability as unknown. src/domain/adb/adbDeviceAnalysis.ts:233-238 defines the replacement request option, and src/contracts/adb/adbToolContracts.ts:288-309 documents the supported install operation and preservation of the device's verdict.

--replace requests replacement mode; it does not establish that a previous installation existed. The result must distinguish that request from an observed replacement. If REA has not established replacement, it must express the uncertainty or identify the value as the requested mode. The output schema declares a boolean at src/domain/adb/adbDeviceAnalysis.ts:682-689 but does not explicitly define it as an observed replacement; the applicable contract is Evidence provenance.

Impact and scope

This is a bounded result-interpretation defect affecting successful installs with replacement mode enabled. A caller cannot use the returned boolean to distinguish a fresh install from a replacement. All three verified CLI calls succeeded, so installation success remains intact in the simulation. No real-device installation, app-data loss, downstream caller decision, or security boundary crossing was established.

A caller can treat replaced as request metadata and use the separately retained parameters.replace to understand the requested mode, without relying on either as proof of replacement. This avoids trusting the inaccurate outcome while retaining the successful install result, but does not correct the emitted Evidence.

REA version

6.4.0 (main at 5830088520)

Runtime and platform

Node.js v24.21.0; macOS arm64. The prepared checkout's development pin is Node.js 24.18.0. Verification did not reinstall dependencies or rebuild the runtime.

Target and artifact context

Stateful executable ADB fake reporting 34.0.5-stub; simulated emulator serial emulator-5554; three-byte local APK placeholder; isolated HOME/XDG directories. No real Android device or emulator was used.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

fresh-install
replacement
fresh-install

fresh-replace {"ok":true,"requested_replace":true,"replaced":true,"confidence":"observed"}
existing-replace {"ok":true,"requested_replace":true,"replaced":true,"confidence":"observed"}
fresh-default {"ok":true,"requested_replace":false,"replaced":false,"confidence":"observed"}

Typed outcome or recovery expected

A successful fresh install must not assert an observed replacement solely because the caller selected --replace. If REA reports an observed replacement outcome, it must distinguish independently established fresh and replacement states. If it only knows the requested mode, it must represent that fact and the uncertainty. Preserve successful fresh installs without --replace and the device's Success/Failure verdict. Rejecting all simulated installs or asserting prior absence without establishing it would not restore the provenance contract.

Analysis or suspected cause

Source-derived at the reported baseline. src/adb/AdbProvider.ts:1822-1896 performs the install and assigns replaced: input.replace at line 1886. src/application/adb/AdbDeviceAnalysisService.ts:22-28 composes ADB results with observed confidence. The CLI registration is src/cli/adbCommands.ts:285-303, and provider dispatch reaches installation at src/adb/AdbProvider.ts:1254-1262. The reproduction exercised local-file, digest, client-selection, and install-verdict checks through this supported entry point.

The existing fake-provider test at src/adb/AdbProvider.test.ts:1154-1180 also uses a small byte fixture and a fake ADB Success verdict. This reproduction establishes simulated-provider CLI behavior, not real-device package installation.

Additional context

P2 under triage/v1. The boolean and observed Evidence classification misstate whether replacement was established, which changes result interpretation. The defect is bounded, a caller can avoid using the boolean as replacement proof, and the verification established no data loss or downstream decision.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3166 ### 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 `rea install-adb-package` returned `normalized_result.replaced: true` with `confidence: observed` for a successful fresh install when the caller supplied `--replace`. A stateful executable ADB fake independently recorded whether each install was fresh or replaced its existing marker. The same command against the already-installed simulated state returned the same replacement boolean. A fresh install without `--replace` returned `replaced: false`. The call log contained client version discovery and installation commands, with no prior-package-state query. The fresh `--replace` result included artifact-identity and host-digest limitations, but none explained that replacement was an unobserved request option. These observations establish the result/provenance mismatch through the supported CLI with a simulated ADB client. ### Reproduction 1. Start from a clean checkout at `58300885207133422af235e0f2b10658cf958b03`, use the development toolchain pinned by `.nvmrc` and `package.json`, and prepare the runtime: ```sh npm ci npm run build:cached ``` 2. Use a new `.repro-install` directory so the installed marker is initially absent. Create the placeholder file and stateful ADB fake: ```sh mkdir -p .repro-install/home/config .repro-install/home/cache .repro-install/home/data printf 'apk' > .repro-install/app.apk cat > .repro-install/adb-stub.sh <<'SH' #!/bin/sh root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) printf '%s\n' "$*" >> "$root/adb-calls.txt" if [ "$1" = version ]; then printf 'Android Debug Bridge version 1.0.41\nVersion 34.0.5-stub\nInstalled as %s\n' "$0" exit 0 fi if [ "$1" = -s ] && [ "$3" = install ]; then if [ -f "$root/installed" ]; then if [ "$4" != -r ]; then printf 'Failure [INSTALL_FAILED_ALREADY_EXISTS]\n' exit 0 fi printf 'replacement\n' >> "$root/device-events.txt" else printf 'fresh-install\n' >> "$root/device-events.txt" fi : > "$root/installed" printf 'Performing Streamed Install\nSuccess\n' exit 0 fi exit 1 SH chmod +x .repro-install/adb-stub.sh ``` 3. Run the CLI twice with `--replace`, then remove the fake's installed marker and run a fresh install without `--replace`. This script gives each CLI child isolated HOME/XDG directories, retains its full JSON, and prints the event log and selected result fields: ```sh node --input-type=module <<'JS' import { readFileSync, writeFileSync, unlinkSync } from 'node:fs'; import { resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; const fixtureRoot = resolve('.repro-install'); const childEnv = { ...process.env, HOME: resolve(fixtureRoot, 'home'), XDG_CONFIG_HOME: resolve(fixtureRoot, 'home/config'), XDG_CACHE_HOME: resolve(fixtureRoot, 'home/cache'), XDG_DATA_HOME: resolve(fixtureRoot, 'home/data'), REA_ADB_PATH: resolve(fixtureRoot, 'adb-stub.sh') }; for (const [name, replace] of [ ['fresh-replace', true], ['existing-replace', true], ['fresh-default', false] ]) { if (name === 'fresh-default') unlinkSync(resolve(fixtureRoot, 'installed')); const args = [ 'scripts/rea.mjs', 'install-adb-package', 'emulator-5554', resolve(fixtureRoot, 'app.apk'), ...(replace ? ['--replace'] : []), '--format', 'json', '--full-output' ]; const child = spawnSync(process.execPath, args, { env: childEnv, encoding: 'utf8' }); if (child.error) throw child.error; if (child.status !== 0) throw new Error(`${name}: ${child.stderr || child.stdout}`); writeFileSync(resolve(fixtureRoot, `${name}.json`), child.stdout); } console.log(readFileSync(resolve(fixtureRoot, 'device-events.txt'), 'utf8').trim()); for (const file of ['fresh-replace', 'existing-replace', 'fresh-default']) { const result = JSON.parse(readFileSync(resolve(fixtureRoot, `${file}.json`), 'utf8')); console.log(file, JSON.stringify({ ok: result.ok, requested_replace: result.data.parameters.replace, replaced: result.data.normalized_result.replaced, confidence: result.data.confidence })); } console.log(readFileSync(resolve(fixtureRoot, 'adb-calls.txt'), 'utf8').trim()); JS ``` 4. Compare the independent event log with the returned fields. The first event is `fresh-install`, yet the first result reports `replaced: true` and `confidence: observed`. The second event is `replacement` with the same returned fields. The third event is `fresh-install` with `replaced: false`. The fake deliberately returns the same successful ADB install text for fresh and replacement states. The three-byte file is a placeholder accepted by REA's file checks and the fake client, rather than a real APK. No Android device is needed. The verifier executed the CLI calls and controls against a prepared baseline. Dependency installation and build are clean-checkout prerequisites and were not rerun during verification. ### Expected behavior `AGENTS.md:5`, under Product and Authority, requires observations, derivations, inferences, and unknowns to remain distinct and Evidence provenance to survive both adapters. `AGENTS.md:19-21`, under Architecture and Cleanup, requires preservation of uncertainty and unresolved applicability as unknown. `src/domain/adb/adbDeviceAnalysis.ts:233-238` defines the replacement request option, and `src/contracts/adb/adbToolContracts.ts:288-309` documents the supported install operation and preservation of the device's verdict. `--replace` requests replacement mode; it does not establish that a previous installation existed. The result must distinguish that request from an observed replacement. If REA has not established replacement, it must express the uncertainty or identify the value as the requested mode. The output schema declares a boolean at `src/domain/adb/adbDeviceAnalysis.ts:682-689` but does not explicitly define it as an observed replacement; the applicable contract is Evidence provenance. ### Impact and scope This is a bounded result-interpretation defect affecting successful installs with replacement mode enabled. A caller cannot use the returned boolean to distinguish a fresh install from a replacement. All three verified CLI calls succeeded, so installation success remains intact in the simulation. No real-device installation, app-data loss, downstream caller decision, or security boundary crossing was established. A caller can treat `replaced` as request metadata and use the separately retained `parameters.replace` to understand the requested mode, without relying on either as proof of replacement. This avoids trusting the inaccurate outcome while retaining the successful install result, but does not correct the emitted Evidence. ### REA version 6.4.0 (main at 58300885207133422af235e0f2b10658cf958b03) ### Runtime and platform Node.js v24.21.0; macOS arm64. The prepared checkout's development pin is Node.js 24.18.0. Verification did not reinstall dependencies or rebuild the runtime. ### Target and artifact context Stateful executable ADB fake reporting 34.0.5-stub; simulated emulator serial `emulator-5554`; three-byte local APK placeholder; isolated HOME/XDG directories. No real Android device or emulator was used. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence ```text fresh-install replacement fresh-install fresh-replace {"ok":true,"requested_replace":true,"replaced":true,"confidence":"observed"} existing-replace {"ok":true,"requested_replace":true,"replaced":true,"confidence":"observed"} fresh-default {"ok":true,"requested_replace":false,"replaced":false,"confidence":"observed"} ``` ### Typed outcome or recovery expected A successful fresh install must not assert an observed replacement solely because the caller selected `--replace`. If REA reports an observed replacement outcome, it must distinguish independently established fresh and replacement states. If it only knows the requested mode, it must represent that fact and the uncertainty. Preserve successful fresh installs without `--replace` and the device's Success/Failure verdict. Rejecting all simulated installs or asserting prior absence without establishing it would not restore the provenance contract. ### Analysis or suspected cause Source-derived at the reported baseline. `src/adb/AdbProvider.ts:1822-1896` performs the install and assigns `replaced: input.replace` at line 1886. `src/application/adb/AdbDeviceAnalysisService.ts:22-28` composes ADB results with observed confidence. The CLI registration is `src/cli/adbCommands.ts:285-303`, and provider dispatch reaches installation at `src/adb/AdbProvider.ts:1254-1262`. The reproduction exercised local-file, digest, client-selection, and install-verdict checks through this supported entry point. The existing fake-provider test at `src/adb/AdbProvider.test.ts:1154-1180` also uses a small byte fixture and a fake ADB Success verdict. This reproduction establishes simulated-provider CLI behavior, not real-device package installation. ### Additional context P2 under triage/v1. The boolean and observed Evidence classification misstate whether replacement was established, which changes result interpretation. The defect is bounded, a caller can avoid using the boolean as replacement proof, and the verification established no data loss or downstream decision.
bsakaguchi 2026-10-15 18:31:04 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Superseded by published main commit 864314ac85. The install contract now records replace_requested as caller intent and leaves replaced: null unless prior package state was observed; it preserves install success/failure, raw output, host digest, and an explicit limitation without adding device queries. The committed ADB provider/CLI/MCP tests distinguish fresh and existing install controls and assert the exact install/version command behavior. No real-device verification is claimed.

<!-- gh-comment-id:6165031775 --> @morluto commented on GitHub (Oct 14, 2026): Superseded by published main commit 864314ac857924a7e6e65bfb37e8dc720fcc21e7. The install contract now records `replace_requested` as caller intent and leaves `replaced: null` unless prior package state was observed; it preserves install success/failure, raw output, host digest, and an explicit limitation without adding device queries. The committed ADB provider/CLI/MCP tests distinguish fresh and existing install controls and assert the exact install/version command behavior. No real-device verification is claimed.
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#1104
No description provided.