Merge pull request #72230 from NousResearch/bb/bootstrap-marker-triage

fix: make the bootstrap-complete marker consistent across every install path
This commit is contained in:
brooklyn! 2026-07-26 17:48:57 -05:00 committed by GitHub
commit 48bdde1deb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 551 additions and 59 deletions

View file

@ -12,11 +12,11 @@
//! 4. Worker iterates stages, calling `install.ps1 -Stage NAME -NonInteractive -Json`.
//! 5. On success → `complete`. On any stage failure → `failed`. On cancel → `failed`.
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, State};
use tokio::sync::{mpsc, Mutex};
@ -260,6 +260,107 @@ pub(crate) fn hermes_is_installed(install_root: &std::path::Path) -> bool {
&& resolve_hermes_desktop_exe(install_root).is_some()
}
fn resolve_marker_commit(install_root: &Path, pin: &Pin) -> Option<String> {
if let Some(commit) = pin
.commit
.as_ref()
.filter(|commit| !commit.trim().is_empty())
{
return Some(commit.clone());
}
let output = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(install_root)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let commit = String::from_utf8_lossy(&output.stdout).trim().to_string();
if commit.is_empty() {
None
} else {
Some(commit)
}
}
fn write_bootstrap_complete_marker(install_root: &Path, pin: &Pin) -> Result<serde_json::Value> {
use std::io::Write;
let marker_path = crate::paths::likely_bootstrap_marker(install_root);
if let Some(parent) = marker_path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!(
"could not create bootstrap marker directory {}",
parent.display()
)
})?;
}
let completed_at_unix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or_default();
let marker = serde_json::json!({
"schemaVersion": 1,
"pinnedCommit": resolve_marker_commit(install_root, pin),
"pinnedBranch": pin.branch.clone(),
"completedAtUnix": completed_at_unix,
});
let mut body = serde_json::to_vec_pretty(&marker)?;
body.push(b'\n');
// Atomic publish (temp sibling + flush + rename), matching Electron's
// writeFileAtomic(). hermes_is_installed() only checks existence, so a
// partial direct write would incorrectly enable the launcher fast path.
let tmp_path = install_root.join(".hermes-bootstrap-complete.tmp");
{
let mut file = std::fs::File::create(&tmp_path).with_context(|| {
format!(
"could not create temp bootstrap marker {}",
tmp_path.display()
)
})?;
file.write_all(&body).with_context(|| {
format!(
"could not write temp bootstrap marker {}",
tmp_path.display()
)
})?;
file.sync_all().with_context(|| {
format!(
"could not flush temp bootstrap marker {}",
tmp_path.display()
)
})?;
}
// Windows rename fails if the destination already exists; drop any prior
// marker first so a re-run can still publish a fresh payload.
if marker_path.exists() {
std::fs::remove_file(&marker_path).with_context(|| {
format!(
"could not replace existing bootstrap marker {}",
marker_path.display()
)
})?;
}
if let Err(err) = std::fs::rename(&tmp_path, &marker_path) {
let _ = std::fs::remove_file(&tmp_path);
return Err(err).with_context(|| {
format!(
"could not publish bootstrap marker {} → {}",
tmp_path.display(),
marker_path.display()
)
});
}
tracing::info!(path = %marker_path.display(), "bootstrap marker written");
Ok(marker)
}
/// Spawn the already-built desktop app, detached. Returns Err if no built app
/// exists or the spawn fails, so the caller can fall back to showing the
/// installer UI.
@ -644,6 +745,23 @@ async fn run_bootstrap(
.unwrap_or_else(|| crate::paths::hermes_home().to_string_lossy().into_owned());
let install_root = PathBuf::from(&hermes_home).join("hermes-agent");
// Marker publish is terminal for this run: a write failure must emit Failed
// so the UI leaves the progress state (it does not poll get_bootstrap_status).
let marker = match write_bootstrap_complete_marker(&install_root, &pin) {
Ok(marker) => marker,
Err(err) => {
let msg = format!("write bootstrap marker failed: {err:#}");
emit_event(
&app,
BootstrapEvent::Failed {
stage: None,
error: msg.clone(),
},
);
return Err(anyhow!(msg));
}
};
// Copy ourselves to HERMES_HOME/hermes-setup.exe so the desktop app can
// re-invoke us with `--update` and shortcuts have a stable target. This is
// a one-shot install concern; an `--update` re-invocation no-ops because
@ -660,10 +778,7 @@ async fn run_bootstrap(
&app,
BootstrapEvent::Complete {
install_root: install_root.to_string_lossy().into_owned(),
marker: Some(serde_json::json!({
"pinnedCommit": pin.commit,
"pinnedBranch": pin.branch,
})),
marker: Some(marker),
},
);
@ -903,4 +1018,103 @@ mod tests {
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn bootstrap_complete_marker_uses_desktop_compatible_schema() {
let root = unique_tmp_dir("marker-schema");
let pin = Pin {
commit: Some("abcdef1234567890".to_string()),
branch: Some("main".to_string()),
};
let marker =
write_bootstrap_complete_marker(&root, &pin).expect("marker write should succeed");
let marker_path = root.join(".hermes-bootstrap-complete");
let from_disk: serde_json::Value =
serde_json::from_slice(&std::fs::read(&marker_path).unwrap()).unwrap();
assert_eq!(marker, from_disk);
assert_eq!(from_disk["schemaVersion"], 1);
assert_eq!(from_disk["pinnedCommit"], "abcdef1234567890");
assert_eq!(from_disk["pinnedBranch"], "main");
assert!(
from_disk["completedAtUnix"].as_u64().is_some(),
"marker must carry a completion timestamp"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn bootstrap_complete_marker_is_published_atomically() {
let root = unique_tmp_dir("marker-atomic");
make_release_tree(&root);
let pin = Pin {
commit: Some("abcdef1234567890".to_string()),
branch: Some("main".to_string()),
};
write_bootstrap_complete_marker(&root, &pin).expect("marker write should succeed");
let marker_path = root.join(".hermes-bootstrap-complete");
let tmp_path = root.join(".hermes-bootstrap-complete.tmp");
assert!(
marker_path.is_file(),
"final marker must exist after atomic publish"
);
assert!(
!tmp_path.exists(),
"temp sibling must not remain after atomic publish"
);
assert!(
hermes_is_installed(&root),
"atomically published marker must enable the installer fast path"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn hermes_is_installed_treats_marker_existence_as_sufficient() {
// Documents why write_bootstrap_complete_marker must publish atomically:
// the launcher predicate only checks existence, so a partial/corrupt
// final marker would still enable the fast path.
let root = unique_tmp_dir("marker-existence-only");
make_release_tree(&root);
std::fs::write(root.join(".hermes-bootstrap-complete"), b"").unwrap();
assert!(
hermes_is_installed(&root),
"empty/partial marker content still counts as installed"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn marker_write_failure_leaves_no_final_marker() {
// install_root is a regular file → create_dir_all on its path fails
// before any marker bytes are published under the final name.
let base = unique_tmp_dir("marker-fail");
let not_a_dir = base.join("not-a-dir");
std::fs::write(&not_a_dir, b"not a directory").unwrap();
let pin = Pin {
commit: Some("abcdef1234567890".to_string()),
branch: Some("main".to_string()),
};
let err = write_bootstrap_complete_marker(&not_a_dir, &pin)
.expect_err("marker write against a non-directory root must fail");
let msg = format!("{err:#}");
assert!(
msg.contains("bootstrap marker"),
"error should mention the marker path: {msg}"
);
assert!(
!not_a_dir.join(".hermes-bootstrap-complete").exists(),
"failed write must not leave a final marker that enables the fast path"
);
assert!(
!not_a_dir.join(".hermes-bootstrap-complete.tmp").exists(),
"failed write must not leave a temp marker sibling either"
);
let _ = std::fs::remove_dir_all(&base);
}
}

View file

@ -149,8 +149,8 @@ fn repair_macos_installer_helper(path: &Path) {
#[cfg(not(target_os = "macos"))]
fn repair_macos_installer_helper(_path: &Path) {}
/// Where install.ps1 writes the bootstrap-complete marker (existence-only file
/// the Electron app also checks). Per main.ts:
/// Where the bootstrap-complete marker lives (existence-only for the Rust
/// installer fast path; JSON schema-checked by the Electron app). Per main.ts:
/// const BOOTSTRAP_COMPLETE_MARKER = path.join(ACTIVE_HERMES_ROOT, '.hermes-bootstrap-complete')
/// We don't always know ACTIVE_HERMES_ROOT until install.ps1 reports it, so
/// this is a probe helper, not a definitive path.

View file

@ -0,0 +1,60 @@
import assert from 'node:assert/strict'
import { test } from 'vitest'
import { classifyActiveRuntime, hasValidBootstrapMarker } from './active-runtime-state'
const VALID_MARKER = {
pinnedCommit: '1234567890abcdef1234567890abcdef12345678',
schemaVersion: 1
}
test('hasValidBootstrapMarker accepts the current schema with a real-looking commit', () => {
assert.equal(hasValidBootstrapMarker(VALID_MARKER, 1), true)
})
test('hasValidBootstrapMarker rejects missing, wrong-schema, and too-short markers', () => {
assert.equal(hasValidBootstrapMarker(null, 1), false)
assert.equal(hasValidBootstrapMarker({ schemaVersion: 2, pinnedCommit: VALID_MARKER.pinnedCommit }, 1), false)
assert.equal(hasValidBootstrapMarker({ schemaVersion: 1, pinnedCommit: 'abc123' }, 1), false)
})
test('classifyActiveRuntime uses a healthy active runtime even when the bootstrap marker is missing', () => {
assert.deepEqual(classifyActiveRuntime(null, 1, true), {
hasValidMarker: false,
shouldUseActiveRuntime: true,
usabilityReason: 'usable'
})
})
test('classifyActiveRuntime uses a healthy active runtime even when the marker is stale or malformed', () => {
assert.deepEqual(classifyActiveRuntime({ schemaVersion: 999, pinnedCommit: 'abc1234' }, 1, true), {
hasValidMarker: false,
shouldUseActiveRuntime: true,
usabilityReason: 'usable'
})
})
test('classifyActiveRuntime refuses an unusable runtime even if a valid marker exists', () => {
assert.deepEqual(classifyActiveRuntime(VALID_MARKER, 1, false), {
hasValidMarker: true,
shouldUseActiveRuntime: false,
usabilityReason: 'unusable'
})
})
test('a CLI-installed runtime with no marker launches instead of re-running bootstrap', () => {
// The reported symptom (#60721): install.sh / install.ps1 produced a healthy
// repo+venv, no desktop-managed marker was ever written, and every launch
// dropped the user back into the first-run installer.
const state = classifyActiveRuntime(null, 1, true)
assert.equal(state.shouldUseActiveRuntime, true, 'a usable runtime must launch')
assert.equal(state.hasValidMarker, false, 'marker provenance stays honest')
})
test('a repair that deleted the marker does not strand a healthy install', () => {
// #72166: the repair handler clears the marker unconditionally. Runtime
// usability, not marker presence, must decide the next boot.
assert.equal(classifyActiveRuntime(null, 1, true).shouldUseActiveRuntime, true)
})

View file

@ -0,0 +1,58 @@
export interface BootstrapMarkerLike {
pinnedCommit?: unknown
schemaVersion?: unknown
}
export interface ActiveRuntimeState {
hasValidMarker: boolean
shouldUseActiveRuntime: boolean
usabilityReason: 'usable' | 'unusable'
}
export function hasValidBootstrapMarker(
marker: BootstrapMarkerLike | null | undefined,
schemaVersion: number
): boolean {
if (!marker || typeof marker !== 'object') {
return false
}
if (marker.schemaVersion !== schemaVersion) {
return false
}
if (typeof marker.pinnedCommit !== 'string' || marker.pinnedCommit.length < 7) {
return false
}
return true
}
// The active install at ~/.hermes/hermes-agent can be real and runnable even if
// Desktop never wrote its first-run bootstrap marker (for example when Hermes
// was installed by the CLI first, or when a past desktop build forgot the
// marker). Runtime usability is authoritative for "can we launch local Hermes
// right now?"; the marker is only provenance about how that install was
// created. A missing/stale marker must never force a healthy local install into
// the first-run bootstrap UI.
export function classifyActiveRuntime(
marker: BootstrapMarkerLike | null | undefined,
schemaVersion: number,
runtimeUsable: boolean
): ActiveRuntimeState {
const hasValidMarker = hasValidBootstrapMarker(marker, schemaVersion)
if (!runtimeUsable) {
return {
hasValidMarker,
shouldUseActiveRuntime: false,
usabilityReason: 'unusable'
}
}
return {
hasValidMarker,
shouldUseActiveRuntime: true,
usabilityReason: 'usable'
}
}

View file

@ -30,6 +30,7 @@ import {
} from 'electron'
import nodePty from 'node-pty'
import { classifyActiveRuntime } from './active-runtime-state'
import { stopBackendChild as stopBackendChildImpl } from './backend-child'
import { dashboardFallbackArgs, sourceDeclaresServe } from './backend-command'
import { createBackendConnectionState } from './backend-connection-state'
@ -1032,6 +1033,12 @@ let remoteReauthFailure = null
// Active first-launch install, so the renderer's Cancel button (and app quit)
// can abort the in-flight install.sh/ps1 instead of leaving it running.
let bootstrapAbortController = null
// Explicit "the user asked for a repair" flag. Repair used to signal intent by
// deleting the bootstrap marker, which stranded healthy installs whose only
// problem was a transient backend error (#72166). Intent now lives here, so
// repair can force the installer without destroying provenance about how the
// install was created. Cleared once the reinstall is under way.
let bootstrapRepairRequested = false
let connectionConfigCache = null
let connectionConfigCacheMtime = null
const hermesLog = []
@ -3383,11 +3390,11 @@ function readJson(filePath) {
}
}
// Bootstrap-complete marker helpers. The marker is written ONCE by the
// first-launch bootstrap runner (Phase 1D) after install.ps1 stages succeed
// AND the user has finished initial configuration. On every subsequent boot
// we check `isBootstrapComplete()` and skip the bootstrap flow entirely if
// the marker is present and current-schema.
// Bootstrap-complete marker helpers. The marker is written by whichever
// installer ran: install.ps1, install.sh, the Rust bootstrap installer, or the
// first-launch bootstrap runner. It is provenance ("a bootstrap finished
// here"), NOT the launch gate -- activeRuntimeState() decides that, because a
// healthy runtime can predate the marker or outlive a repair that cleared it.
//
// Marker schema (version 1):
// {
@ -3420,29 +3427,13 @@ function isActiveRuntimeUsable() {
)
}
function isBootstrapComplete() {
const marker = readBootstrapMarker()
if (!marker || typeof marker !== 'object') {
return false
}
if (marker.schemaVersion !== BOOTSTRAP_MARKER_SCHEMA_VERSION) {
return false
}
if (typeof marker.pinnedCommit !== 'string' || marker.pinnedCommit.length < 7) {
return false
}
function activeRuntimeState() {
// We DELIBERATELY do NOT verify that the checkout is currently at the
// pinned commit -- users update via the in-app update path or `hermes
// update`, which moves HEAD legitimately. The marker just attests "we
// ran the bootstrap successfully at least once." We DO additionally require
// a runnable venv: an interrupted or split-home install can leave the marker
// + checkout without a venv, and trusting that spawns a dead backend
// ("gateway offline") instead of re-running bootstrap to repair it.
return isActiveRuntimeUsable()
// update`, which moves HEAD legitimately. The marker only attests "a
// desktop-managed bootstrap ran here at least once"; runtime usability is
// what decides whether we can actually launch.
return classifyActiveRuntime(readBootstrapMarker(), BOOTSTRAP_MARKER_SCHEMA_VERSION, isActiveRuntimeUsable())
}
function writeBootstrapMarker(payload) {
@ -3702,16 +3693,30 @@ function resolveHermesBackend(backendArgs) {
}
}
// 3. Bootstrap-complete ACTIVE_HERMES_ROOT -- the canonical install at
// %LOCALAPPDATA%\hermes\hermes-agent (Windows) or ~/.hermes/hermes-agent.
// The bootstrap marker means install.ps1 stages finished and the user
// completed initial configuration; we trust the install and go straight
// to spawning hermes. Updates flow through the in-app update path
// (applyUpdates -> git pull) or `hermes update` from the CLI.
if (isBootstrapComplete()) {
// 3. ACTIVE_HERMES_ROOT — the canonical install at
// %LOCALAPPDATA%\\hermes\\hermes-agent (Windows) or ~/.hermes/hermes-agent.
// A valid bootstrap marker proves Desktop finished the first-run install
// flow, but marker provenance is NOT the same thing as runtime usability:
// the CLI can create the exact same repo+venv layout, and older desktop
// builds could leave a healthy install behind without the marker. If the
// active runtime is usable, launch it directly; only fall through to
// bootstrap when the runtime itself is unusable.
const activeRuntime = activeRuntimeState()
if (activeRuntime.shouldUseActiveRuntime && !bootstrapRepairRequested) {
if (!activeRuntime.hasValidMarker) {
rememberLog(
`[bootstrap] Active Hermes runtime at ${ACTIVE_HERMES_ROOT} is usable but the bootstrap marker is missing or stale; skipping first-run bootstrap.`
)
}
return createActiveBackend(backendArgs)
}
if (bootstrapRepairRequested) {
rememberLog('[bootstrap] repair requested; bypassing the usable active runtime to re-run the installer')
}
// 4. Existing `hermes` on PATH -- installed via install.ps1 / install.sh from
// a previous tool-only setup, or pip-installed system-wide. Use it but
// do NOT write a bootstrap marker; the user did this themselves and we
@ -3885,6 +3890,10 @@ async function ensureRuntime(backend) {
bootstrapAbortController = new AbortController()
// The repair request has been honoured by reaching the installer; clear it
// so a later boot isn't forced through bootstrap again.
bootstrapRepairRequested = false
const bootstrapResult = await runBootstrap({
installStamp: backend.installStamp,
activeRoot: backend.activeRoot,
@ -3979,10 +3988,10 @@ async function ensureRuntime(backend) {
// No venv at the expected location AND no bootstrap-needed sentinel
// means we have a half-installed checkout: .git exists, source files
// exist, but venv is missing or broken. This shouldn't happen in
// normal flow because isBootstrapComplete() requires
// isHermesSourceRoot() and the bootstrap writes the marker only after
// install.ps1 succeeds. If we hit this, the user (or a deleted venv)
// broke the invariant; tell them to re-run the install.
// normal flow because activeRuntimeState() requires isHermesSourceRoot()
// plus an importable hermes_cli before it hands back the active runtime.
// If we hit this, the user (or a deleted venv) broke the invariant; tell
// them to re-run the install.
throw new Error(
`Hermes venv missing at ${VENV_ROOT}. Re-run the desktop installer or ` + '`scripts/install.ps1` to rebuild it.'
)
@ -9106,20 +9115,17 @@ ipcMain.handle('hermes:bootstrap:reset', async () => {
return { ok: true }
})
ipcMain.handle('hermes:bootstrap:repair', async () => {
// Forceful repair: drop the bootstrap-complete marker so the next
// startHermes() re-runs the full installer (refreshing a broken/partial
// venv), and clear any latched failure + live connection. The renderer
// reloads afterwards to re-drive the boot flow from scratch.
rememberLog('[bootstrap] repair requested by renderer; clearing marker + latched failure')
try {
if (fileExists(BOOTSTRAP_COMPLETE_MARKER)) {
fs.rmSync(BOOTSTRAP_COMPLETE_MARKER, { force: true })
}
} catch (error) {
rememberLog(`[bootstrap] failed to remove marker during repair: ${error.message}`)
}
// Forceful repair: force the next startHermes() through the full installer
// (refreshing a broken/partial venv) and clear any latched failure + live
// connection. The renderer reloads afterwards to re-drive the boot flow.
//
// We do NOT delete the bootstrap marker here. Repair is also reachable from
// transient backend errors on a perfectly healthy install, and deleting the
// marker in that case stranded the app in first-run setup with no way back
// (#72166). The explicit flag carries the intent instead.
rememberLog('[bootstrap] repair requested by renderer; forcing reinstall + clearing latched failure')
bootstrapRepairRequested = true
bootstrapFailure = null
backendStartFailure = null
remoteReauthFailure = null

View file

@ -2391,6 +2391,48 @@ maybe_start_gateway() {
fi
}
write_bootstrap_marker() {
# Writes $INSTALL_DIR/.hermes-bootstrap-complete, which tells the Hermes
# desktop app (apps/desktop/electron/main.ts) and the macOS launcher fast
# path (apps/bootstrap-installer) "a real install finished here -- don't
# re-run first-run bootstrap."
#
# Schema mirrors install.ps1's Write-BootstrapMarker and main.ts's
# writeBootstrapMarker(). Keep the three in lockstep:
# schemaVersion 1 + pinnedCommit (length >= 7) are what the desktop
# validator requires; desktopVersion is omitted because only the desktop
# app knows its own version.
if [ ! -d "$INSTALL_DIR" ]; then
log_warn "Skipping bootstrap marker: $INSTALL_DIR doesn't exist"
return 0
fi
# Explicit --commit wins; otherwise read HEAD from the checkout we just
# installed. If neither resolves, skip the marker entirely rather than
# write one the desktop will reject -- an absent marker is a clean
# "bootstrap needed", a malformed one is a confusing half-state.
local pinned_commit="$INSTALL_COMMIT"
if [ -z "$pinned_commit" ]; then
pinned_commit=$(git -C "$INSTALL_DIR" rev-parse HEAD 2>/dev/null) || pinned_commit=""
fi
if [ -z "$pinned_commit" ]; then
log_warn "Skipping bootstrap marker: could not resolve HEAD in $INSTALL_DIR"
return 0
fi
local marker_path="$INSTALL_DIR/.hermes-bootstrap-complete"
local tmp_path="$marker_path.tmp"
# Atomic publish: the macOS launcher predicate only checks existence, so a
# torn write would arm the fast path against a half-written marker.
printf '{\n "schemaVersion": 1,\n "pinnedCommit": "%s",\n "pinnedBranch": "%s",\n "completedAt": "%s"\n}\n' \
"$pinned_commit" \
"$BRANCH" \
"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" > "$tmp_path"
mv -f "$tmp_path" "$marker_path"
}
print_success() {
echo ""
echo -e "${GREEN}${BOLD}"
@ -3024,6 +3066,7 @@ run_stage_body() {
detect_os
resolve_install_layout
print_success
write_bootstrap_marker
# Code-scoped stamp: write next to the install tree, not into
# $HERMES_HOME. $HERMES_HOME is a shared data dir (it can be
# bind-mounted into a Docker gateway too), so a stamp there gets
@ -3108,6 +3151,8 @@ main() {
print_success
write_bootstrap_marker
# Code-scoped stamp: write next to the install tree, not into $HERMES_HOME.
# $HERMES_HOME is a shared data dir (it can be bind-mounted into a Docker
# gateway too), so a stamp there gets clobbered by the container's 'docker'

View file

@ -0,0 +1,109 @@
"""install.sh must stamp the desktop bootstrap-complete marker.
The marker at ``$INSTALL_DIR/.hermes-bootstrap-complete`` is what the desktop
app (apps/desktop/electron/main.ts) and the macOS launcher fast path
(apps/bootstrap-installer) use to decide "a real install finished here."
install.sh never wrote it, so a CLI-installed Mac/Linux box re-ran first-run
bootstrap on every desktop launch (#60721).
These exercise the real shell function against a temp checkout rather than
asserting on the text of install.sh.
"""
import json
import subprocess
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
INSTALL_SH = REPO_ROOT / "scripts" / "install.sh"
def run_write_marker(install_dir, *, commit="", branch="main"):
"""Source install.sh and invoke write_bootstrap_marker in isolation.
install.sh guards its own entrypoint behind MANIFEST_MODE/STAGE_NAME/main,
so sourcing it with --help-less argv defines the functions without running
an install.
"""
script = f"""
set -e
INSTALL_DIR={install_dir!s}
INSTALL_COMMIT={commit!r}
BRANCH={branch!r}
# Pull in the function definitions without triggering an install.
eval "$(sed -n '/^write_bootstrap_marker()/,/^}}/p' {INSTALL_SH!s})"
log_warn() {{ echo "WARN: $*" >&2; }}
write_bootstrap_marker
"""
return subprocess.run(
["bash", "-c", script], capture_output=True, text=True, timeout=30
)
def make_checkout(tmp_path):
install_dir = tmp_path / "hermes-agent"
install_dir.mkdir()
subprocess.run(["git", "init", "-q"], cwd=install_dir, check=True)
subprocess.run(
["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q",
"--allow-empty", "-m", "init"],
cwd=install_dir,
check=True,
)
return install_dir
def test_marker_matches_the_schema_the_desktop_validates(tmp_path):
"""Desktop's isBootstrapComplete() needs schemaVersion 1 + a >=7 char commit."""
install_dir = make_checkout(tmp_path)
result = run_write_marker(install_dir)
assert result.returncode == 0, result.stderr
marker = install_dir / ".hermes-bootstrap-complete"
assert marker.is_file(), "install.sh must stamp the bootstrap marker"
payload = json.loads(marker.read_text())
assert payload["schemaVersion"] == 1
assert len(payload["pinnedCommit"]) >= 7
assert payload["pinnedBranch"] == "main"
assert payload["completedAt"].endswith("Z")
def test_marker_publish_leaves_no_temp_sibling(tmp_path):
"""The launcher predicate is existence-only, so the write must be atomic."""
install_dir = make_checkout(tmp_path)
run_write_marker(install_dir)
assert (install_dir / ".hermes-bootstrap-complete").is_file()
assert not (install_dir / ".hermes-bootstrap-complete.tmp").exists()
def test_explicit_commit_pin_wins_over_head(tmp_path):
install_dir = make_checkout(tmp_path)
pinned = "abcdef1234567890abcdef1234567890abcdef12"
run_write_marker(install_dir, commit=pinned)
payload = json.loads((install_dir / ".hermes-bootstrap-complete").read_text())
assert payload["pinnedCommit"] == pinned
def test_no_marker_written_when_head_cannot_be_resolved(tmp_path):
"""A malformed marker is worse than none: absent means a clean re-bootstrap."""
install_dir = tmp_path / "not-a-checkout"
install_dir.mkdir()
result = run_write_marker(install_dir)
assert result.returncode == 0, "an unresolvable HEAD must not fail the install"
assert not (install_dir / ".hermes-bootstrap-complete").exists()
def test_missing_install_dir_is_not_fatal(tmp_path):
result = run_write_marker(tmp_path / "does-not-exist")
assert result.returncode == 0