From ddc8c31514ffda0216d3d64b64569eb7143d4d5e Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Sun, 1 Nov 2026 22:50:40 -0800
Subject: [PATCH] test(install-e2e): HEAD -> NEXT legs accept the reused
desktop app's build commit
Every HEAD -> NEXT install/update E2E leg (Linux, macOS, Windows) failed with
"Running commit
does not equal expected ". The synthetic NEXT
commit adds only a marker file, so since 7a954418ceb `hermes update` keeps the
packaged desktop app ("no desktop build input changed"), and that app keeps
naming the commit it was built from by design.
The smoke now accepts the app reporting NEXT's parent only when the installed
commit is a source checkout whose single change from that parent is the
synthetic marker file. Any real commit still requires the app to report it,
so a stale app after a real desktop change stays a failure.
---
tests-js/scripts/desktop-chat-smoke.ts | 11 +++--
.../desktop-reused-build-commit.test.ts | 48 +++++++++++++++++++
tests-js/scripts/desktop-smoke-process.ts | 28 +++++++++++
tests/install/e2e-assets/desktop-smoke.ts | 5 +-
4 files changed, 86 insertions(+), 6 deletions(-)
create mode 100644 tests-js/scripts/desktop-reused-build-commit.test.ts
diff --git a/tests-js/scripts/desktop-chat-smoke.ts b/tests-js/scripts/desktop-chat-smoke.ts
index 50dad62844..528d010379 100644
--- a/tests-js/scripts/desktop-chat-smoke.ts
+++ b/tests-js/scripts/desktop-chat-smoke.ts
@@ -16,6 +16,8 @@ export interface DesktopChatSmokeOptions {
outDir: string
expectCommit?: string
provenanceCommit?: string
+ /** A commit the running app may report instead of expectCommit: the build a reused packaged app came from. */
+ reusedBuildCommit?: string
observePrompts?: () => Promise
}
@@ -75,9 +77,10 @@ export async function readChatIdentity(page: Page): Promise {
return { ...identity, commit: identity.commit ?? null }
}
-/** Old desktops do not report a commit; their caller must verify the installed runtime. */
-export function assertChatCommit(identity: ChatIdentity, expected: string, provenanceCommit?: string): void {
- if (identity.commit !== null && identity.commit !== expected) {
+/** Old desktops do not report a commit; their caller must verify the installed runtime.
+ * A reused packaged app reports the commit it was built from (`reusedBuildCommit`). */
+export function assertChatCommit(identity: ChatIdentity, expected: string, provenanceCommit?: string, reusedBuildCommit?: string): void {
+ if (identity.commit !== null && identity.commit !== expected && identity.commit !== reusedBuildCommit) {
throw new Error(`Running commit ${identity.commit} does not equal expected ${expected}`)
}
@@ -279,7 +282,7 @@ export async function runDesktopChatSmoke(page: Page, options: DesktopChatSmokeO
const identity = await readChatIdentity(page)
- if (expectCommit) { assertChatCommit(identity, expectCommit, options.provenanceCommit) }
+ if (expectCommit) { assertChatCommit(identity, expectCommit, options.provenanceCommit, options.reusedBuildCommit) }
const beforeIds = (await readTranscript(page)).map((message: TranscriptMessage): string => message.id)
const before = (await observe()).length
await composer.click()
diff --git a/tests-js/scripts/desktop-reused-build-commit.test.ts b/tests-js/scripts/desktop-reused-build-commit.test.ts
new file mode 100644
index 0000000000..2f9424c1cd
--- /dev/null
+++ b/tests-js/scripts/desktop-reused-build-commit.test.ts
@@ -0,0 +1,48 @@
+import { spawnSync } from 'node:child_process'
+import fs from 'node:fs'
+import os from 'node:os'
+import path from 'node:path'
+
+import { expect, test } from 'vitest'
+
+import { assertChatCommit } from './desktop-chat-smoke.ts'
+import { reusedDesktopBuildCommit } from './desktop-smoke-process.ts'
+
+function git(repo: string, ...args: string[]): string {
+ const result = spawnSync('git', ['-C', repo, ...args], {
+ encoding: 'utf8',
+ env: { ...process.env, GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t.invalid', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t.invalid' },
+ })
+ if (result.status !== 0) { throw new Error(result.stderr) }
+
+ return result.stdout.trim()
+}
+
+function commitFile(repo: string, name: string): string {
+ fs.writeFileSync(path.join(repo, name), `${name}\n`)
+ git(repo, 'add', name)
+ git(repo, 'commit', '-q', '-m', name)
+
+ return git(repo, 'rev-parse', 'HEAD')
+}
+
+test('a reused packaged app may report its build commit only across the synthetic NEXT commit', (): void => {
+ const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'reused-build-'))
+
+ try {
+ git(repo, 'init', '-q')
+ const head = commitFile(repo, 'app.ts')
+ const next = commitFile(repo, '.hermes-e2e-next')
+ const app = { appVersion: '0.0.0', commit: head, hermesRoot: repo, platform: process.platform }
+
+ // HEAD -> NEXT: only the marker changed, so the app built at HEAD stands for NEXT.
+ const reused = reusedDesktopBuildCommit(repo, 'source', next)
+ expect(reused).toBe(head)
+ expect((): void => assertChatCommit(app, next, next, reused)).not.toThrow()
+
+ // A commit that changes anything else gives no allowance: a stale app stays a failure.
+ const real = commitFile(repo, 'feature.ts')
+ expect(reusedDesktopBuildCommit(repo, 'source', real)).toBeUndefined()
+ expect((): void => assertChatCommit({ ...app, commit: next }, real, real, reusedDesktopBuildCommit(repo, 'source', real))).toThrow('does not equal expected')
+ } finally { fs.rmSync(repo, { recursive: true, force: true }) }
+})
diff --git a/tests-js/scripts/desktop-smoke-process.ts b/tests-js/scripts/desktop-smoke-process.ts
index 34bdc39176..9191c451b0 100644
--- a/tests-js/scripts/desktop-smoke-process.ts
+++ b/tests-js/scripts/desktop-smoke-process.ts
@@ -63,6 +63,34 @@ export function readInstallationCommit(root: string, origin: 'source' | 'bundled
return z.string().regex(/^[0-9a-f]{40}$/).parse(commit)
}
+/** The file the HEAD -> NEXT legs' synthetic commit adds (installer-common.sh::resolve_update_ref,
+ * windows-e2e.ps1::New-NextCommit). */
+const SYNTHETIC_NEXT_MARKER = '.hermes-e2e-next'
+
+/** The commit a reused packaged app may still report for an installed source checkout.
+ *
+ * `hermes update` keeps the packaged app when no desktop build input changed, and that app keeps
+ * naming the commit it was built from (hermes_cli/desktop_build_reuse.py). The harness can prove
+ * that case only for the synthetic NEXT commit, whose sole change is one marker file: there the
+ * parent is the commit the app may report. Any other installed commit, and any bundled install,
+ * returns undefined. */
+export function reusedDesktopBuildCommit(root: string, origin: 'source' | 'bundled', installed: string): string | undefined {
+ if (origin !== 'source') {
+ return undefined
+ }
+
+ const git: string = process.env.HERMES_E2E_REAL_GIT || 'git'
+ const parents = nativeText(git, ['-C', root, 'rev-list', '--parents', '-n', '1', installed]).split(/\s+/).slice(1)
+
+ if (parents.length !== 1) {
+ return undefined
+ }
+
+ const changed = nativeText(git, ['-C', root, 'diff', '--name-only', parents[0], installed])
+
+ return changed === SYNTHETIC_NEXT_MARKER ? parents[0] : undefined
+}
+
const bundleEnvSchema = z.record(z.string(), z.string().nullable())
/** The baked runtime defaults/clears of a bundled artifact, recorded in the
diff --git a/tests/install/e2e-assets/desktop-smoke.ts b/tests/install/e2e-assets/desktop-smoke.ts
index 273ba4d65f..4261e40a44 100644
--- a/tests/install/e2e-assets/desktop-smoke.ts
+++ b/tests/install/e2e-assets/desktop-smoke.ts
@@ -12,7 +12,7 @@ import { z } from 'zod'
import { resolveDesktopHermesHome } from '../../../apps/desktop/electron/data-paths.mjs'
import { applyBundleEnvironment } from '../../../apps/desktop/scripts/bundle-env.mjs'
import { type ChatIdentity, readChatIdentity, runDesktopChatSmoke, waitForChatReady } from '../../../tests-js/scripts/desktop-chat-smoke.ts'
-import { assertBackendOrigin, localBackendProcess, readBundledBundleEnv, readInstallationCommit } from '../../../tests-js/scripts/desktop-smoke-process.ts'
+import { assertBackendOrigin, localBackendProcess, readBundledBundleEnv, readInstallationCommit, reusedDesktopBuildCommit } from '../../../tests-js/scripts/desktop-smoke-process.ts'
import { validateMockUrl, writeEnvFile, writeMockProviderConfig } from '../../../tests-js/scripts/mock-provider-config.ts'
import { type MockServer, startMockServer } from '../../../tests-js/scripts/mock-server.ts'
@@ -353,7 +353,8 @@ export async function runInstalledDesktopSmoke(options: SmokeOptions, launchApp:
if (provenanceCommit !== options['expect-commit']) { throw new Error('Installed commit differs from --expect-commit') }
// Trace only chat actions. Connection probes can return credential-bearing logs.
await app.context().tracing.start({ screenshots: true, snapshots: false, sources: false })
- const chat = await runDesktopChatSmoke(page, { mockUrl, phase: options.phase, outDir: out, expectCommit: options['expect-commit'], provenanceCommit })
+ const reusedBuildCommit = reusedDesktopBuildCommit(options.root, options.origin, provenanceCommit)
+ const chat = await runDesktopChatSmoke(page, { mockUrl, phase: options.phase, outDir: out, expectCommit: options['expect-commit'], provenanceCommit, reusedBuildCommit })
fs.writeFileSync(receiptPath, `${JSON.stringify({ ...chat, status: 'closing' }, null, 2)}\n`)
const result = { ...chat, origin: options.origin, root: options.root, executable: options.exe, running, predictedHome,
localModeConfigured: true, backend: { ...backend, command: redact(backend.command) }, launchKind: options.phase === 'new' ? 'post-update-launch' : 'installed-launch' }