~cytrogen/gstack

2d88f5f02a0a808243fefd0b3d817c887793cece — Garry Tan a month ago c6c3294
test: add update-check exit code regression tests

Guards against the "exits 1 when up to date" bug that broke skill
preambles. Two new tests: real VERSION + unreachable remote, and
multi-call sequence verifying exit 0 in all states.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 files changed, 48 insertions(+), 0 deletions(-)

M browse/test/gstack-update-check.test.ts
M browse/test/gstack-update-check.test.ts => browse/test/gstack-update-check.test.ts +48 -0
@@ 185,4 185,52 @@ describe('gstack-update-check', () => {
    expect(exitCode).toBe(0);
    expect(existsSync(join(newStateDir, 'last-update-check'))).toBe(true);
  });

  // ─── E2E regression: always exit 0 ───────────────────────────
  // Agents call this on every skill invocation. Exit code 1 breaks
  // the preamble and confuses the agent. This test guards against
  // regressions like the "exits 1 when up to date" bug.
  test('exits 0 with real project VERSION and unreachable remote', () => {
    // Simulate agent context: real VERSION file, network unavailable
    const projectRoot = join(import.meta.dir, '..', '..');
    const versionFile = join(projectRoot, 'VERSION');
    if (!existsSync(versionFile)) return; // skip if no VERSION
    const version = readFileSync(versionFile, 'utf-8').trim();

    // Copy VERSION into test dir
    writeFileSync(join(gstackDir, 'VERSION'), version + '\n');

    // Remote is unreachable (simulates offline / CI / sandboxed agent)
    const { exitCode, stdout } = run({
      GSTACK_REMOTE_URL: 'file:///nonexistent/path/VERSION',
    });
    expect(exitCode).toBe(0);
    // Should write UP_TO_DATE cache (not crash)
    const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
    expect(cache).toContain('UP_TO_DATE');
  });

  test('exits 0 when up to date (not exit 1)', () => {
    // Regression test: script previously exited 1 when versions matched.
    // This broke every skill preamble that called it without || true.
    writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
    writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n');

    // First call: fetches remote, writes cache
    const first = run();
    expect(first.exitCode).toBe(0);
    expect(first.stdout).toBe('');

    // Second call: reads fresh cache
    const second = run();
    expect(second.exitCode).toBe(0);
    expect(second.stdout).toBe('');

    // Third call with upgrade available: still exit 0
    writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
    rmSync(join(stateDir, 'last-update-check')); // force re-fetch
    const third = run();
    expect(third.exitCode).toBe(0);
    expect(third.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
  });
});