fix(update-check): surface failed preamble checks

This commit is contained in:
maxpetrusenkoagent 2026-06-11 20:31:43 -04:00
parent cab774cced
commit d7478031f9
3 changed files with 87 additions and 10 deletions

View File

@ -4,6 +4,7 @@
# Output (one line, or nothing):
# JUST_UPGRADED <old> <new> — marker found from recent upgrade
# UPGRADE_AVAILABLE <old> <new> — remote VERSION differs from local
# CHECK_FAILED exit=<n> cmd=<cmd> — abnormal script failure; do not infer up-to-date
# (nothing) — up to date, snoozed, disabled, or check skipped
#
# Env overrides (for testing):
@ -11,7 +12,19 @@
# GSTACK_REMOTE_URL — override remote VERSION URL (branch-pinned fallback)
# GSTACK_REMOTE_REPO — override remote git URL for ls-remote SHA resolution
# GSTACK_STATE_DIR — override ~/.gstack state directory
set -euo pipefail
set -Eeuo pipefail
report_check_failed() {
local status="$?"
local cmd="${BASH_COMMAND:-unknown}"
cmd="${cmd//$'\n'/ }"
echo "CHECK_FAILED exit=$status cmd=$cmd"
# Keep CHECK_FAILED as a stdout sentinel, not a failing process status.
# The generated preamble runs this command inside an `||` fallback chain;
# nonzero here would run the fallback and can duplicate or obscure the marker.
exit 0
}
trap report_check_failed ERR
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
@ -172,12 +185,14 @@ _SUPA_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
_TEL_TIER="$("$GSTACK_DIR/bin/gstack-config" get telemetry 2>/dev/null || true)"
if [ -n "$_SUPA_URL" ] && [ -n "$_SUPA_KEY" ] && [ "${_TEL_TIER:-off}" != "off" ]; then
_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
curl -sf --max-time 5 \
-X POST "${_SUPA_URL}/functions/v1/update-check" \
-H "Content-Type: application/json" \
-H "apikey: ${_SUPA_KEY}" \
-d "{\"version\":\"$LOCAL\",\"os\":\"$_OS\"}" \
>/dev/null 2>&1 &
(
curl -sf --max-time 5 \
-X POST "${_SUPA_URL}/functions/v1/update-check" \
-H "Content-Type: application/json" \
-H "apikey: ${_SUPA_KEY}" \
-d "{\"version\":\"$LOCAL\",\"os\":\"$_OS\"}" \
>/dev/null 2>&1 || true
) &
fi
# Resolve VERSION via a SHA-pinned raw URL. GitHub's branch-raw CDN
@ -241,8 +256,10 @@ fi
# Log upgrade_prompted event (only on slow-path fetch, not cached replays)
TEL_CMD="$GSTACK_DIR/bin/gstack-telemetry-log"
if [ -x "$TEL_CMD" ]; then
"$TEL_CMD" --event-type upgrade_prompted --skill "" --duration 0 \
--outcome success --session-id "update-$$-$(date +%s)" 2>/dev/null &
(
"$TEL_CMD" --event-type upgrade_prompted --skill "" --duration 0 \
--outcome success --session-id "update-$$-$(date +%s)" 2>/dev/null || true
) &
fi
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE"

View File

@ -7,7 +7,7 @@
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync, mkdirSync, symlinkSync, utimesSync } from 'fs';
import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync, mkdirSync, symlinkSync, utimesSync, chmodSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
@ -476,6 +476,23 @@ describe('gstack-update-check', () => {
expect(cache).toContain('UP_TO_DATE');
});
test('--force emits CHECK_FAILED when state files cannot be removed', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
writeFileSync(join(stateDir, 'last-update-check'), 'UP_TO_DATE 0.3.3');
writeFileSync(join(stateDir, 'update-snoozed'), `0.4.0 1 ${nowEpoch()}`);
chmodSync(stateDir, 0o555);
try {
const { exitCode, stdout } = run({}, ['--force']);
expect(exitCode).toBe(0);
expect(stdout).toContain('CHECK_FAILED');
expect(stdout).toContain('rm');
} finally {
chmodSync(stateDir, 0o755);
}
});
test('--force clears snooze so user can upgrade after snoozing', () => {
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');

View File

@ -296,6 +296,49 @@ describe('Update check preamble', () => {
expect(result.exitCode).toBe(0);
expect(result.stdout.toString().trim()).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
});
test('update check bash block surfaces CHECK_FAILED when script crashes', () => {
const tempRoot = fs.mkdtempSync(path.join(ROOT, '.tmp-update-check-'));
const gstackDir = path.join(tempRoot, 'gstack');
const stateDir = path.join(tempRoot, 'state');
const binDir = path.join(gstackDir, 'bin');
const fallbackBinDir = path.join(tempRoot, '.agents', 'skills', 'gstack', 'bin');
fs.mkdirSync(binDir, { recursive: true });
fs.mkdirSync(fallbackBinDir, { recursive: true });
fs.mkdirSync(stateDir, { recursive: true });
fs.writeFileSync(path.join(gstackDir, 'VERSION'), '0.3.3\n');
fs.writeFileSync(path.join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
fs.writeFileSync(path.join(stateDir, '.codex-desc-healed'), '');
fs.writeFileSync(path.join(stateDir, 'just-upgraded-from'), '0.2.0\n');
fs.symlinkSync(path.join(ROOT, 'bin', 'gstack-config'), path.join(binDir, 'gstack-config'));
fs.symlinkSync(path.join(ROOT, 'bin', 'gstack-update-check'), path.join(binDir, 'gstack-update-check'));
fs.symlinkSync(path.join(ROOT, 'bin', 'gstack-update-check'), path.join(fallbackBinDir, 'gstack-update-check'));
fs.chmodSync(stateDir, 0o555);
try {
const result = Bun.spawnSync(['bash', '-c',
'_UPD=$($GSTACK_BIN/gstack-update-check 2>/dev/null || .agents/skills/gstack/bin/gstack-update-check 2>/dev/null || true); [ -n "$_UPD" ] && echo "$_UPD" || true'
], {
cwd: tempRoot,
env: {
...process.env,
GSTACK_BIN: binDir,
GSTACK_DIR: gstackDir,
GSTACK_STATE_DIR: stateDir,
GSTACK_REMOTE_URL: `file://${path.join(gstackDir, 'REMOTE_VERSION')}`,
},
stdout: 'pipe',
stderr: 'pipe',
});
expect(result.exitCode).toBe(0);
expect(result.stdout.toString().trim()).toContain('CHECK_FAILED');
expect(result.stdout.toString().trim()).toContain('rm');
} finally {
fs.chmodSync(stateDir, 0o755);
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
});
// --- Part 7: Cross-skill path consistency (A1) ---