diff --git a/CHANGELOG.md b/CHANGELOG.md index 09d3cac92..5f1a13758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.93.13.0] - 2026-11-02 + +**gstack's own tests no longer write into a developer's real gbrain.** If you export `GBRAIN_DATABASE_URL` for your working brain and run gstack's test suite, tests that spawn bins with a temp `HOME` used to pass that URL straight through. With no gbrain config under the temp `HOME`, the real `gbrain` on your PATH then added `brain-sync-remote-*` sources and test pages to your live brain. From #3466 by @AndreIntelas. + +### Fixed +- **The test preload (`test-setup.ts`) strips the variables gbrain uses to pick a brain** before any test module loads: `GBRAIN_DATABASE_URL`, `GBRAIN_DIRECT_DATABASE_URL`, `DATABASE_URL` and `GBRAIN_HOME`. It prints one line on stderr for each variable it strips. This covers `bun test ` and every shard of `bun run test`. + +### Tests +- `test/state-root-config-merge.test.ts`: a `bun test` run with all four variables exported spawns a child under a temp `HOME` that calls `buildGbrainEnv`, and the child sees none of them. It fails on main. + ## [1.93.12.0] - 2026-11-02 **The Opus 5.5 `lite` skills now keep the install's gbrain blocks, disabled skills and the Claims Need Evidence rule (GST-944).** The lite tree Capy publishes was rendered without the install's settings. On a machine with gbrain detected, Opus 5.5 sessions lost the brain context-load and save-results blocks that the full install serves, and skills turned off with `disabled_skills` showed up again in the lite router. `./setup --loadout lite` was not affected. diff --git a/VERSION b/VERSION index 7f209dd81..c2fc44987 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.93.12.0 +1.93.13.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 513f2a765..923c3f576 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.93.12.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.93.13.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/package.json b/package.json index 098af7b3f..4c4f97952 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.93.12", + "version": "1.93.13", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/test-setup.ts b/test-setup.ts index 31234cc63..a9c6fc020 100644 --- a/test-setup.ts +++ b/test-setup.ts @@ -39,6 +39,15 @@ for (const name of ['GSTACK_STATE_ROOT', 'GSTACK_STATE_DIR']) { // an unattended Capy session it never set up. The shard engine strips the same // list for the lanes; this reaches direct runs too. for (const name of CAPY_LAUNCH_MARKERS) delete process.env[name]; +// The variables gbrain reads to pick a brain (#3466). Tests spawn bins with +// { ...process.env, HOME: }; with no gbrain config under the temp HOME, +// buildGbrainEnv passes an exported URL through and a real gbrain on PATH +// writes test pages and sources into the developer's live brain. +for (const name of ['GBRAIN_DATABASE_URL', 'GBRAIN_DIRECT_DATABASE_URL', 'DATABASE_URL', 'GBRAIN_HOME']) { + if (process.env[name] === undefined) continue; + delete process.env[name]; + process.stderr.write(`test-setup: stripped inherited ${name}; tests must not reach a live gbrain\n`); +} process.env.GSTACK_TEST_LEGACY_ROOT = path.join(os.tmpdir(), `gstack-test-legacy-root-${process.pid}-${Date.now()}-unused`); // Workflow leases (bin/gstack-lease) default to the installation's git dir; // for the dev checkout under test that is this repo's .git, and a live test diff --git a/test/state-root-config-merge.test.ts b/test/state-root-config-merge.test.ts index a0e5cd73e..d6a1a25e5 100644 --- a/test/state-root-config-merge.test.ts +++ b/test/state-root-config-merge.test.ts @@ -163,4 +163,42 @@ describe('test preload hermeticity', () => { expect(r.stderr).toContain('test-setup: stripped inherited GSTACK_STATE_DIR; tests isolate state via GSTACK_HOME'); expect(r.stderr.match(/test-setup: stripped/g)?.length).toBe(2); }); + + test('bun test strips an inherited live-brain URL, so a temp-HOME gbrain child never sees it (#3466)', () => { + const live = path.join(tmp, 'live-gbrain'); + fs.mkdirSync(live, { recursive: true }); + fs.writeFileSync(path.join(live, 'config.json'), JSON.stringify({ database_url: 'postgresql://live-config.invalid/brain' })); + const childHome = path.join(tmp, 'child-home'); + fs.mkdirSync(childHome, { recursive: true }); + const probe = path.join(tmp, 'brain-probe.test.ts'); + fs.writeFileSync(probe, [ + "import { test, expect } from 'bun:test';", + "import { spawnSync } from 'child_process';", + "test('probe', () => {", + " const script = \"const { buildGbrainEnv } = require(\" + JSON.stringify(" + JSON.stringify(path.join(ROOT, 'lib', 'gbrain-exec.ts')) + ") + \"); const e = buildGbrainEnv(); console.log(['GBRAIN_DATABASE_URL','GBRAIN_DIRECT_DATABASE_URL','DATABASE_URL','GBRAIN_HOME'].map(k => k + '=' + (e[k] ?? '')).join(' '));\";", + " const r = spawnSync(process.execPath, ['--no-env-file', '--config=/dev/null', '-e', script], {", + " env: { ...process.env, HOME: " + JSON.stringify(childHome) + " }, encoding: 'utf-8', timeout: 30_000,", + " });", + " expect(r.stdout.trim()).toBe('GBRAIN_DATABASE_URL= GBRAIN_DIRECT_DATABASE_URL= DATABASE_URL= GBRAIN_HOME=');", + "});", + ].join('\n')); + const r = spawnSync('bun', ['test', '--preload', path.join(ROOT, 'test-setup.ts'), probe], { + cwd: tmp, + env: { + ...env, GSTACK_HOME: resolved, + GBRAIN_DATABASE_URL: 'postgresql://live-brain.invalid/brain', + GBRAIN_DIRECT_DATABASE_URL: 'postgresql://live-brain.invalid/direct', + DATABASE_URL: 'postgresql://live-brain.invalid/fallback', + GBRAIN_HOME: live, + }, + encoding: 'utf-8', + timeout: 60_000, + }); + expect(r.stderr).not.toContain('live-brain.invalid'); + expect(r.stderr).not.toContain('live-config.invalid'); + expect(r.status).toBe(0); + for (const name of ['GBRAIN_DATABASE_URL', 'GBRAIN_DIRECT_DATABASE_URL', 'DATABASE_URL', 'GBRAIN_HOME']) { + expect(r.stderr).toContain(`test-setup: stripped inherited ${name}; tests must not reach a live gbrain`); + } + }); });