diff --git a/tests-js/update-ui.test.mjs b/tests-js/update-ui.test.mjs index a55434c385..3906cb7fd0 100644 --- a/tests-js/update-ui.test.mjs +++ b/tests-js/update-ui.test.mjs @@ -74,7 +74,8 @@ test('checks the live Desktop bridge against the staged target before opening Ab const f = fixture({ statusOverride: { supported: true, branch: 'main', currentSha: 'b'.repeat(40), targetSha: sha, behind: 1, dirty: false } }) await updateUi.assertStagedBranch(f.page, sha, f.log) expect(f.log).toHaveBeenCalledWith(expect.stringContaining('"targetSha"')) - const current = fixture({ statusOverride: { supported: true, branch: 'main', currentSha: 'b'.repeat(40), targetSha: sha, behind: 1, updateAvailable: true } }) + // The current checker's hosted shape: it cannot count staged commits via GitHub compare. + const current = fixture({ statusOverride: { supported: true, branch: 'main', currentSha: 'b'.repeat(40), targetSha: sha, behind: null, commits: [], updateAvailable: true } }) await updateUi.assertStagedBranch(current.page, sha, current.log) const racing = fixture() let checks = 0 @@ -90,6 +91,7 @@ test('checks the live Desktop bridge against the staged target before opening Ab { supported: true, branch: 'main', targetSha: sha, behind: 0 }, { supported: true, branch: 'main', currentSha: sha, targetSha: sha, behind: 1 }, { supported: true, branch: 'main', targetSha: sha, behind: 1, updateAvailable: false }, + { supported: true, branch: 'main', currentSha: 'b'.repeat(40), targetSha: sha, behind: null }, ]) { const refused = fixture({ statusOverride }) await expect(updateUi.assertStagedBranch(refused.page, sha, refused.log)).rejects.toThrow(/refusing to click/) diff --git a/tests/install/e2e-assets/source-driver.sh b/tests/install/e2e-assets/source-driver.sh index 7067b18d00..3bda813bcc 100644 --- a/tests/install/e2e-assets/source-driver.sh +++ b/tests/install/e2e-assets/source-driver.sh @@ -20,6 +20,21 @@ source_hermes() { printf '%s\n' "$command" } +# v2026.6.19's install.sh (also run by its DMG bootstrap) writes .install_method +# into the checkout without ignoring it, and that release's Desktop checker +# reports git status --porcelain verbatim. Teach only this disposable clone that +# the installer's own marker is not a source edit; keep the marker for +# install-method detection and refuse any other dirty state. +accept_installer_marker() { + local root="$1" status + status="$(git -C "$root" status --porcelain --untracked-files=all)" || return 1 + if [ "$status" = '?? .install_method' ] && [ "$(cat "$root/.install_method")" = git ]; then + printf '\n/.install_method\n' >> "$(git -C "$root" rev-parse --absolute-git-dir)/info/exclude" + status="$(git -C "$root" status --porcelain --untracked-files=all)" || return 1 + fi + [ -z "$status" ] || { printf 'installed source has changes other than the installer marker:\n%s\n' "$status" >&2; return 1; } +} + # Hand out a command to DRIVE the next ordinary startup, even when the # published launcher is not there yet. # diff --git a/tests/install/e2e-assets/update-ui.cjs b/tests/install/e2e-assets/update-ui.cjs index 62e2e5e8bb..e80856f98b 100644 --- a/tests/install/e2e-assets/update-ui.cjs +++ b/tests/install/e2e-assets/update-ui.cjs @@ -90,12 +90,16 @@ async function assertStagedBranch(page, expectedSha, log) { await page.waitForTimeout(1_000) } // Historical Desktop status has no updateAvailable field: its About/overlay - // offers the button when behind > 0. Never accept an explicit false from a - // newer checker, a dirty source tree, or merely a matching remote tip. + // offers the button when behind > 0. A newer checker states updateAvailable + // and leaves behind null when it cannot count (GitHub compare does not know a + // staged commit). Never accept an explicit false, a dirty source tree, or + // merely a matching remote tip. + const offered = status.updateAvailable === undefined + ? Number.isInteger(status.behind) && status.behind > 0 + : status.updateAvailable === true if (status.supported !== true || status.error || status.dirty === true || status.branch !== 'main' || status.targetSha !== expectedSha || - !Number.isInteger(status.behind) || status.behind <= 0 || - status.currentSha === expectedSha || status.updateAvailable === false) { + status.currentSha === expectedSha || !offered) { throw new Error('Desktop source check did not offer staged Git main; refusing to click an unrelated update') } } diff --git a/tests/install/installer-script-e2e.sh b/tests/install/installer-script-e2e.sh index a00d127eb8..9f90322bc4 100755 --- a/tests/install/installer-script-e2e.sh +++ b/tests/install/installer-script-e2e.sh @@ -452,18 +452,7 @@ case "$UPDATE_METHOD" in # fixup) runs for real in the installed code. EXPECT_DESKTOP=present HERMES="$(source_hermes "$INSTALL_DIR")" || fail "no installed desktop command" - # v2026.6.19's installer writes this marker into its checkout without - # ignoring it. Its Electron checker reports git status --porcelain verbatim; - # teach only this disposable clone that the installer's own state is not - # a source edit. Keep the marker for install-method detection and refuse - # any other dirty state rather than masking it for the GUI update. - marker_status="$(git -C "$INSTALL_DIR" status --porcelain --untracked-files=all)" - if [ "$marker_status" = '?? .install_method' ] && - [ "$(cat "$INSTALL_DIR/.install_method")" = git ]; then - printf '\n/.install_method\n' >> "$(git -C "$INSTALL_DIR" rev-parse --absolute-git-dir)/info/exclude" - ok "ignored only the installer's generated .install_method in local Git exclude" - fi - [ -z "$(git -C "$INSTALL_DIR" status --porcelain --untracked-files=all)" ] \ + accept_installer_marker "$INSTALL_DIR" \ || fail "installed source has changes other than the generated install marker" ASSETS="$REPO_ROOT/tests/install/e2e-assets" SPEC="$WORK_ROOT/launch-spec.json" diff --git a/tests/install/macos-desktop-e2e.sh b/tests/install/macos-desktop-e2e.sh index f3970ccaa9..4a096a05b1 100755 --- a/tests/install/macos-desktop-e2e.sh +++ b/tests/install/macos-desktop-e2e.sh @@ -271,6 +271,8 @@ run_playwright_update() { # $1: spec file to launch from. local spec="$1" local rc=0 + accept_installer_marker "$INSTALL_DIR" \ + || fail "installed source has changes other than the generated install marker" (cd "$WORK_ROOT" && "$HERMES_E2E_NODE" "$ASSETS/launch-from-spec.mjs" \ --spec "$spec" \ --old-sha "$OLD_SHA" --chat-out "$LOG_DIR/update-window" --mock-url "$HERMES_E2E_MOCK_URL" \ diff --git a/tests/scripts/test_source_driver.py b/tests/scripts/test_source_driver.py index 224fee9e7d..1978a30a69 100644 --- a/tests/scripts/test_source_driver.py +++ b/tests/scripts/test_source_driver.py @@ -47,6 +47,31 @@ if source_hermes "$INSTALL_DIR"; then exit 92; fi assert result.stdout.splitlines() == ["published:literal argument", "legacy"] +@pytest.mark.platforms("posix") +def test_installer_marker_is_the_only_dirty_state_a_driver_accepts(tmp_path): + root = tmp_path / "installed source" + root.mkdir() + git = ["git", "-C", str(root), "-c", "user.name=t", "-c", "user.email=t@t"] + subprocess.run([*git, "init", "-q"], check=True) + (root / "tracked").write_text("v1\n", encoding="utf-8") + subprocess.run([*git, "add", "tracked"], check=True) + subprocess.run([*git, "commit", "-qm", "base"], check=True) + (root / ".install_method").write_text("git\n", encoding="utf-8") + env = dict(os.environ, INSTALL_DIR=str(root), ASSETS=str(ASSETS), HOME=str(tmp_path)) + + def accept(): + return subprocess.run(["bash", "-euc", 'source "$ASSETS/source-driver.sh"; accept_installer_marker "$INSTALL_DIR"'], + env=env, capture_output=True, text=True, timeout=30) + + assert accept().returncode == 0 + assert (root / ".install_method").read_text(encoding="utf-8") == "git\n" + assert subprocess.run([*git, "status", "--porcelain", "--untracked-files=all"], + capture_output=True, text=True, check=True).stdout == "" + (root / "tracked").write_text("edited\n", encoding="utf-8") + refused = accept() + assert refused.returncode != 0 and "M tracked" in refused.stderr + + @pytest.mark.platforms("windows", "posix") def test_powershell_selects_exact_exe_or_cmd_and_legacy_fallback(tmp_path): # These are file-selection rules, not Windows execution emulation. Run