mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
Packaged Desktop first-launch bootstrap no longer dies with a fatal HTTP 404 when install-stamp.json pins a commit that isn't fetchable from GitHub. This only happens for locally-built desktop apps: write-build-stamp.cjs's fromLocalGit() pins `git rev-parse HEAD`, which can be an unpushed commit or dirty tree. CI builds stamp $GITHUB_SHA and are unaffected. The fix unblocks the dev / self-builder workflow. resolveInstallScript() now wraps the GitHub download in try/catch; on failure it resolves ~/.hermes/hermes-agent/scripts/install.sh (the already-installed agent checkout), copies it into bootstrap-cache, and returns it as source 'installed-agent'. If the cache copy fails (read-only FS), it uses the source path directly. With no installed checkout to fall back to, the original error rethrows unchanged. Download is now injectable via an optional _download param so the fallback path is tested hermetically (no network). Reported with a precise repro and suggested fix by @Tamaz-sujashvili (#40815). Co-authored-by: Tamaz-sujashvili <56168197+Tamaz-sujashvili@users.noreply.github.com>
138 lines
4.4 KiB
JavaScript
138 lines
4.4 KiB
JavaScript
const assert = require('node:assert/strict')
|
|
const test = require('node:test')
|
|
const fs = require('node:fs')
|
|
const os = require('node:os')
|
|
const path = require('node:path')
|
|
|
|
const {
|
|
runBootstrap,
|
|
resolveInstallScript,
|
|
installedAgentInstallScript,
|
|
cachedScriptPath
|
|
} = require('./bootstrap-runner.cjs')
|
|
|
|
const SCRIPT_NAME = process.platform === 'win32' ? 'install.ps1' : 'install.sh'
|
|
|
|
function mkTmpHome() {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-bootstrap-test-'))
|
|
}
|
|
|
|
test('runBootstrap bails immediately when the signal is already aborted', async () => {
|
|
const controller = new AbortController()
|
|
controller.abort()
|
|
|
|
const events = []
|
|
const result = await runBootstrap({
|
|
installStamp: null,
|
|
activeRoot: '/tmp/hermes-runner-test',
|
|
sourceRepoRoot: null,
|
|
hermesHome: '/tmp/hermes-runner-test',
|
|
logRoot: '/tmp/hermes-runner-test',
|
|
onEvent: ev => events.push(ev),
|
|
abortSignal: controller.signal
|
|
})
|
|
|
|
// Cancelled before any install script is spawned.
|
|
assert.deepEqual(result, { ok: false, cancelled: true })
|
|
assert.ok(
|
|
events.some(ev => ev.type === 'failed' && /cancelled/i.test(ev.error)),
|
|
'should emit a cancelled failure event'
|
|
)
|
|
})
|
|
|
|
test('installedAgentInstallScript resolves the installer in the agent checkout', () => {
|
|
const home = mkTmpHome()
|
|
try {
|
|
assert.equal(installedAgentInstallScript(home), null, 'absent before the checkout exists')
|
|
|
|
const scriptsDir = path.join(home, 'hermes-agent', 'scripts')
|
|
fs.mkdirSync(scriptsDir, { recursive: true })
|
|
const scriptPath = path.join(scriptsDir, SCRIPT_NAME)
|
|
fs.writeFileSync(scriptPath, '#!/bin/sh\necho hi\n')
|
|
|
|
assert.equal(installedAgentInstallScript(home), scriptPath)
|
|
assert.equal(installedAgentInstallScript(null), null, 'null home -> null')
|
|
} finally {
|
|
fs.rmSync(home, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('resolveInstallScript prefers a cached script without touching the network', async () => {
|
|
const home = mkTmpHome()
|
|
try {
|
|
const commit = 'a'.repeat(40)
|
|
const cached = cachedScriptPath(home, commit)
|
|
fs.mkdirSync(path.dirname(cached), { recursive: true })
|
|
fs.writeFileSync(cached, '#!/bin/sh\necho cached\n')
|
|
|
|
const logs = []
|
|
const result = await resolveInstallScript({
|
|
installStamp: { commit },
|
|
sourceRepoRoot: null,
|
|
hermesHome: home,
|
|
emit: ev => logs.push(ev)
|
|
})
|
|
|
|
assert.equal(result.source, 'cache')
|
|
assert.equal(result.path, cached)
|
|
} finally {
|
|
fs.rmSync(home, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('resolveInstallScript falls back to the installed agent checkout on a 404', async () => {
|
|
const home = mkTmpHome()
|
|
try {
|
|
const commit = 'a'.repeat(40)
|
|
// Seed the installed agent checkout so the fallback has something to resolve.
|
|
const scriptsDir = path.join(home, 'hermes-agent', 'scripts')
|
|
fs.mkdirSync(scriptsDir, { recursive: true })
|
|
const installed = path.join(scriptsDir, SCRIPT_NAME)
|
|
fs.writeFileSync(installed, '#!/bin/sh\necho fallback\n')
|
|
|
|
const logs = []
|
|
const result = await resolveInstallScript({
|
|
installStamp: { commit },
|
|
sourceRepoRoot: null,
|
|
hermesHome: home,
|
|
emit: ev => logs.push(ev),
|
|
// Simulate GitHub returning a 404 for the pinned commit.
|
|
_download: async () => {
|
|
throw new Error('Failed to download install.sh: HTTP 404')
|
|
}
|
|
})
|
|
|
|
assert.equal(result.source, 'installed-agent')
|
|
// It should have copied the installer into the bootstrap cache.
|
|
assert.equal(result.path, cachedScriptPath(home, commit))
|
|
assert.ok(fs.existsSync(result.path), 'fallback script copied into cache')
|
|
assert.ok(
|
|
logs.some(ev => /falling back to installed agent/.test(ev.line || '')),
|
|
'emits a fallback log line'
|
|
)
|
|
} finally {
|
|
fs.rmSync(home, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('resolveInstallScript rethrows when the 404 fallback is unavailable', async () => {
|
|
const home = mkTmpHome()
|
|
try {
|
|
const commit = 'a'.repeat(40)
|
|
// No installed agent checkout seeded -> nothing to fall back to.
|
|
await assert.rejects(
|
|
resolveInstallScript({
|
|
installStamp: { commit },
|
|
sourceRepoRoot: null,
|
|
hermesHome: home,
|
|
emit: () => {},
|
|
_download: async () => {
|
|
throw new Error('Failed to download install.sh: HTTP 404')
|
|
}
|
|
}),
|
|
/HTTP 404|Failed to download/
|
|
)
|
|
} finally {
|
|
fs.rmSync(home, { recursive: true, force: true })
|
|
}
|
|
})
|