* fix(#4728): stop presenting the retired Gemini CLI as a supported runtime
#1928 removed the Gemini CLI runtime after Google sunset it on 2026-06-18, and
updated the ENGLISH docs. The locale mirrors and the runtime-loaded workflow
prose were not updated in the same change, and no gate asserts the ABSENCE of a
retired runtime, so both drifted quietly for a year.
The finding that shaped this change: English is already correct. docs/
ARCHITECTURE.md, CONFIGURATION.md, USER-GUIDE.md, how-to/install-on-your-runtime.md
and CLI-TOOLS.md carry zero runtime-axis Gemini references; the only English hits
anywhere are a Gemini 2.5 Pro MODEL line, the GEMINI_API_KEY row, and prose that
correctly documents the retirement. So the docs half of this is translation lag,
not a content decision, and every locale edit here is parity with an existing
English line rather than new wording:
- install-on-your-runtime.md English has NO `### Gemini CLI` section -> deleted
- USER-GUIDE.md :843 "…, Antigravity CLI, Kilo)" -> substituted
- ARCHITECTURE.md English has NO Gemini CLI table row -> row deleted
- ARCHITECTURE.md :24 English holds `Kimi CLI` in that slot -> Kimi CLI
- context-monitor.md :3 "`AfterTool` for Antigravity CLI" -> substituted
- spike-and-sketch.md :93 "(Codex, Antigravity CLI, etc.)" -> substituted
- configure-model-profiles "Codex, OpenCode, Antigravity CLI, or Kilo" -> substituted
- COMMANDS.md English keeps only hyphen + Codex bullets -> colon bullet deleted
- FEATURES.md source docs/features/multi-runtime-support.md:10
lists no Gemini CLI -> name removed
ARCHITECTURE.md:24 is the clearest case for reading English rather than
substituting blind: Antigravity ALREADY appears later in that list, so replacing
Gemini CLI with Antigravity would have named it twice. English holds Kimi CLI
there, so that is what the locales get.
The largest single class was hand-duplicated boilerplate. A "Text mode" paragraph
repeated across 34 runtime-loaded workflow files ends "…required for non-Claude
runtimes (OpenAI Codex, Gemini CLI, etc.)". No lint enforces that sentence and no
script syncs it, so every copy was edited. These files are read by the agent at
runtime, so they steer behavior rather than only informing a reader — which is why
this class matters more than its word count suggests.
The slash-command-form section is restructured in all four languages to match
English, which had already dropped its colon-form bullet. That bullet claimed the
colon form is "Gemini CLI only", which was false on its own terms independent of
the retirement: `/gsd:…` is GSD's canonical AUTHORING token, rewritten per runtime
at install time, and NO runtime registers it — VALID_COMMAND_STYLES is
{slash-hyphen, shell-var} and 18 of 19 runtimes declare slash-hyphen. Substituting
the runtime name would have left the claim false with Antigravity's name in it, so
the claim is gone, matching English.
Two anchor regressions were caught and fixed while doing that. zh-CN lost its
explicit {#slash-command-forms-hyphen-vs-colon} anchor while its TOC still linked
it; the anchor is restored. ko-KR and pt-BR never had an explicit anchor and rely
on the slug generated from the heading text, so shortening the heading broke their
own TOC links; those links now point at the new slugs. English's heading lost its
anchor while its TOC still links the old one — that latent English bug is
deliberately NOT copied.
Preserved, because `gemini` is not one thing here and a blanket sweep breaks the
product: ~/.gemini/antigravity{,-ide,-cli} and ~/.gemini as their parent;
~/.gemini/config (#3738); GEMINI.md; hookEvents "gemini"; GEMINI_API_KEY in all
four locales; every gemini-* model id and the Gemini 2.5 Pro references in
ko-KR/pt-BR/zh-CN (ja-JP genuinely lacks that line — the locales have diverged, so
a uniform patch would be wrong); the hook-event dialect notes, which are
RE-ATTRIBUTED rather than deleted because Antigravity inherits that dialect;
reapply-patches.md:93's legacy-install note; host-integration-capability-matrix.md
:27 and :342, which correctly record the sunset and Antigravity's contract;
whats-new-1.7.0.md and FEATURES.md:3506, which document the retirement itself; and
the generated launcher preamble, which belongs to epic #4632 — zero
_GSD_SHIM_NAME lines appear in this diff.
Coverage: a #4728 block in tests/gemini-runtime-removed.test.cjs asserts the
retired name is gone from STRUCTURAL POSITIONS (a level-3 heading, a table row's
first cell, a runtime-example parenthetical) rather than asserting the string is
absent, which would be wrong. It pairs those with positive PRESERVE assertions
over the same files — Antigravity's heading, ~/.gemini/antigravity, GEMINI_API_KEY,
AfterTool — so a patch that deletes too much fails as loudly as one that deletes
too little. The model-axis test pins both the presence in three locales and the
absence in ja-JP, so a later uniform patch that "helpfully" adds it back fails.
The new docs/ reads tripped lint-docs-guard-registration for the first time in
this file, so the test is registered in scripts/docs-guard-registry.cjs.
Not covered here, by design: nothing above would catch a Gemini-as-runtime
reference appearing in a NEW file tomorrow. That is the repo-wide drift guard,
#4729, which must land last — written now it would red on the very references this
change removes.
Fixes #4728
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(#4728): fix four review blockers, including a vacuous test and my own duplicate
A full matrix run on 31f12d7943 FAILED with 3 real failures, and an isolated
adversarial review returned BLOCK on four blockers. All of it was correct.
1. I committed the exact error I claimed to have avoided. The commit message
boasted that ARCHITECTURE.md:24 proved the value of reading English rather
than substituting blind, because Antigravity already appeared later in that
list. Five hundred lines further down the SAME four files, my
`Gemini:` -> `Antigravity:` substitution produced TWO consecutive
`- Antigravity:` bullets, because an Antigravity bullet was already there.
English (ARCHITECTURE.md:827) merges them into one. Now merged in all four
locales, reusing each locale's existing words.
2. `--gemini` survived in the runtime-detection CLI flag list in all four
locale ARCHITECTURE.md files. English:817 holds `--kimi` in that slot and
already lists `--antigravity` later, so this is another place where
substituting Antigravity would have duplicated it. Now `--kimi`.
3. Two runtime-loaded workflow files still enumerated Gemini one line ABOVE the
line I had already corrected -- the "Adaptive (Recommended)" option in
settings.md:192 and new-project/steps/auto-mode-config.md:95.
4. THE NEW TEST WAS VACUOUS for two of its five files. It matched only
`non-Claude runtimes (` and `(e.g. `, and neither regex could reach the two
lines the change actually fixed: health.md:52 reads `non-Claude (Codex, ...)`
without the word "runtimes", and execute-phase.md:1028 has no parenthetical
at all. The reviewer proved it by re-introducing Gemini at both lines and
watching the assertion stay GREEN. That same blind spot is what hid finding 3.
Replaced with a case-sensitive `/\bGemini\b/` walk over every
`gsd-core/workflows/**/*.md`, which works because every LEGITIMATE gemini
reference in that tree is spelled differently and cannot match: Antigravity's
paths are lowercase with a slash (`~/.gemini/antigravity`), Google's model ids
are lowercase and hyphenated (`gemini-3.1-pro-preview`), and the env vars are
uppercase (`GEMINI_CONFIG_DIR`, `GEMINI_SESSION_ID`). A bare capitalised
`Gemini` there means the retired RUNTIME is being named. The walk asserts it
found at least 50 files so an empty walk cannot pass vacuously, and it now
covers the nested `new-project/steps/` directory where finding 3 lived.
Two allowlist entries, both by line CONTENT and both justified:
reapply-patches.md's `Legacy: ... pre-#1928` note, and settings-advanced.md's
`Known provider` menu. The second was escalated by the agent rather than
decided: Section 8 of that file says model policy is defined "independently"
of the runtime, so `(Claude / OpenAI / Gemini / Qwen)` is the PROVIDER axis --
the same axis as the lowercase model ids -- and must keep working.
Proven to fail, not just asserted: the predicate reports 0 offenders on the
real tree and exactly 2 on a /tmp copy with Gemini re-injected at
health.md:52 and execute-phase.md:1028.
Also from the review: a `| Gemini |` COLUMN survived in the locale FEATURES.md
comparison tables (English has none) -- removed from all three, with header,
separator and every body row kept aligned; two ENGLISH runtime-axis sites were
missed by my own parity standard (how-to/execute-a-phase.md:88 and
how-to/verify-and-ship.md:89, the latter doubly stale since #4716 retired the
Gemini reviewer lane); docs/USER-GUIDE.md:12 linked a dead anchor, which I had
found and deliberately left -- record-and-proceed on a known defect is exactly
what the rules forbid, so it is fixed; docs/COMMANDS.md:12 and all four mirrors
still claimed "the hyphen and colon forms are runtime-specific spellings" with
no colon form documented anywhere, so that false sentence is deleted; and ko-KR
had the installer rather than the user doing the targeting.
The other two matrix failures were the compact-content benchmark baseline, which
drifted because this PR changes byte counts, refreshed via the script's own
`--write` path rather than by hand; and this commit's emitted-drift-ack trailers.
Method note on the acks: the failing run measured growth against
origin/next@1110c3b4ee, which is the STALE LOCAL `next` ref -- gsd-test merges
into the local base branch, and this machine's `next` is seven commits behind
origin/next, which is checked out in the main worktree and so cannot be
fast-forwarded from here. The 32 trailers below are computed against the REAL
base (origin/next @ ca8d9d4459) by comparing each tracked file's blob size, which
is one more file than that run reported -- the extra is settings.md, grown again
by fix 3. docs-update.md and map-codebase.md are deliberately NOT acked: they
SHRANK, since there the fix deleted ", Gemini CLI" rather than substituting, and
acking a file no delta consumed is itself an error.
Refs #4728
Emitted-Drift-Ack-Growth: add-tests.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: add-todo.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: ai-integration-phase.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: check-todos.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: cleanup.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: complete-milestone.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: do.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: eval-review.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: execute-phase.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: execute-plan.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: health.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: import.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: inbox.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: manager.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: new-milestone.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: new-workspace.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: note.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: onboard.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: plant-seed.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: profile-user.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: quick.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: remove-workspace.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: secure-phase.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: settings.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: ship.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: smart-entry.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: ui-phase.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: ui-review.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: undo.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: update.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: validate-phase.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: verify-work.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(#4728): add the changeset fragment
The PR body claimed one was present and it was not — caught by
scripts/changeset/lint.cjs reporting fail_missing_fragment, not by the
checklist, which is exactly why the lint exists.
Type Fixed: the diff is prose, and a docs-only fix uses Fixed since there is
no Documentation type.
Refs #4728
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
763 lines
38 KiB
JavaScript
763 lines
38 KiB
JavaScript
/**
|
|
* #1928 — Gemini CLI runtime removal + Antigravity redirect.
|
|
*
|
|
* Google sunset Gemini CLI on 2026-06-18; Antigravity CLI is the official
|
|
* successor. GSD removes the `gemini` runtime and turns `--gemini` into an
|
|
* explicit deprecation redirect (NOT a silent alias — Hyrum's Law, per the
|
|
* issue's rejected alternative #2).
|
|
*
|
|
* Coverage:
|
|
* A. CLI redirect contract (spawned installer): the sunset notice, the
|
|
* no-silent-install failure path, clean UX (no stack trace), and that a
|
|
* co-selected valid runtime still installs.
|
|
* B. The `gemini` runtime is gone from every runtime-name-policy surface.
|
|
* C. Antigravity is PRESERVED everywhere it shared surface with gemini
|
|
* (GEMINI.md instruction file + the shared convertAntigravityToolName
|
|
* tool vocabulary) — the shared-infra regression this change had to avoid.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { describe, test, before } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
const fs = require('node:fs');
|
|
const { spawnSync } = require('node:child_process');
|
|
const { runNode } = require('./helpers/process-seam.cjs');
|
|
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
|
|
|
const { createTempDir, cleanup } = require('./helpers.cjs');
|
|
const { runMinimalInstall, BUILD_SCRIPT } = require('./helpers/install-shared.cjs');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
const INSTALL_JS = path.join(ROOT, 'bin', 'install.js');
|
|
|
|
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
|
const { BUILD_TIMEOUT_MS: BUILD_HOOKS_TIMEOUT_MS, INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
|
|
|
// hooks/dist is gitignored + built; build it idempotently so a real install
|
|
// emits hooks (mirrors golden-install-parity / install-minimal-hooks).
|
|
before(() => {
|
|
throwIfFailed(runNode([BUILD_SCRIPT], { timeoutMs: BUILD_HOOKS_TIMEOUT_MS }), `node ${BUILD_SCRIPT}`);
|
|
});
|
|
|
|
const {
|
|
canonicalizeRuntimeName,
|
|
getRuntimeLabel,
|
|
getGlobalConfigHomeFragment,
|
|
getRuntimeNewProjectCommand,
|
|
runtimeFlags,
|
|
getProjectInstructionFile,
|
|
} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-name-policy.cjs'));
|
|
|
|
const registry = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'capability-registry.cjs'));
|
|
const catalog = require(path.join(ROOT, 'gsd-core', 'bin', 'shared', 'model-catalog.json'));
|
|
|
|
const { convertClaudeAgentToAntigravityAgent } = require('../bin/install.js');
|
|
|
|
// Run the installer as a subprocess with an isolated HOME so no install can
|
|
// touch the real machine. Runtime-config env overrides are stripped so the
|
|
// child resolves config dirs strictly under the temp HOME.
|
|
function runInstaller(args, homeDir) {
|
|
const env = { ...process.env, HOME: homeDir, USERPROFILE: homeDir, GSD_TEST_MODE: '1' };
|
|
for (const k of [
|
|
'CLAUDE_CONFIG_DIR', 'GEMINI_CONFIG_DIR', 'ANTIGRAVITY_CONFIG_DIR',
|
|
'XDG_CONFIG_HOME', 'CODEX_CONFIG_DIR', 'OPENCODE_CONFIG_DIR', 'KILO_CONFIG_DIR',
|
|
]) delete env[k];
|
|
return spawnSync(process.execPath, [INSTALL_JS, ...args], {
|
|
cwd: homeDir, env, encoding: 'utf8', timeout: INSTALL_TIMEOUT_MS,
|
|
});
|
|
}
|
|
|
|
describe('#1928 --gemini CLI deprecation redirect', () => {
|
|
test('--gemini alone prints the sunset notice and exits non-zero without installing', (t) => {
|
|
const home = createTempDir('gsd-1928-gemini-only-');
|
|
t.after(() => cleanup(home));
|
|
|
|
const r = runInstaller(['--gemini'], home);
|
|
const out = `${r.stdout || ''}${r.stderr || ''}`;
|
|
|
|
assert.strictEqual(r.status, 1, 'a bare --gemini must exit 1, not silently fall through to a Claude install');
|
|
assert.match(out, /sunset by Google on 2026-06-18/, 'must cite the 2026-06-18 sunset date');
|
|
assert.match(out, /--antigravity/, 'must redirect the user to --antigravity');
|
|
assert.match(out, /Antigravity CLI \(the official successor\)/);
|
|
// No silent install: nothing was written under the isolated HOME.
|
|
assert.ok(!fs.existsSync(path.join(home, '.gemini')), 'must not create a .gemini runtime dir');
|
|
assert.ok(!fs.existsSync(path.join(home, '.claude')), 'bare --gemini must not silently install Claude');
|
|
});
|
|
|
|
test('--gemini --global still exits 1 (removed flag regardless of scope)', (t) => {
|
|
const home = createTempDir('gsd-1928-gemini-global-');
|
|
t.after(() => cleanup(home));
|
|
|
|
const r = runInstaller(['--gemini', '--global'], home);
|
|
assert.strictEqual(r.status, 1);
|
|
assert.match(`${r.stdout || ''}${r.stderr || ''}`, /sunset by Google on 2026-06-18/);
|
|
});
|
|
|
|
test('the redirect is a clean message — no stack trace leaks to the user', (t) => {
|
|
const home = createTempDir('gsd-1928-gemini-clean-');
|
|
t.after(() => cleanup(home));
|
|
|
|
const r = runInstaller(['--gemini'], home);
|
|
const err = r.stderr || '';
|
|
assert.doesNotMatch(err, /^\s+at .+:\d+:\d+/m, 'no V8 stack frame in redirect output');
|
|
assert.doesNotMatch(err, /\bError:|\bTypeError:|\bthrow\b/, 'no thrown-error prose in redirect output');
|
|
});
|
|
|
|
test('--gemini --help still prints usage (the redirect must not suppress help)', (t) => {
|
|
const home = createTempDir('gsd-1928-gemini-help-');
|
|
t.after(() => cleanup(home));
|
|
|
|
const r = runInstaller(['--gemini', '--help'], home);
|
|
assert.strictEqual(r.status, 0, '--help must exit 0, not the redirect error code');
|
|
assert.match(`${r.stdout || ''}`, /Usage:/, 'the usage/help block must still print to stdout');
|
|
assert.match(`${r.stderr || ''}`, /sunset by Google on 2026-06-18/, 'the notice also prints');
|
|
});
|
|
|
|
test('--gemini --uninstall guides manual cleanup and does NOT uninstall Claude', (t) => {
|
|
const home = createTempDir('gsd-1928-gemini-uninstall-');
|
|
t.after(() => cleanup(home));
|
|
|
|
// Sentinel: a pre-existing Claude install that must survive. Run WITHOUT
|
|
// GSD_TEST_MODE so the real uninstall dispatch is active — the redirect must
|
|
// exit before it (the dispatch defaults an empty selection to 'claude').
|
|
fs.mkdirSync(path.join(home, '.claude'), { recursive: true });
|
|
fs.writeFileSync(path.join(home, '.claude', 'sentinel.txt'), 'keep me');
|
|
const env = { ...process.env, HOME: home, USERPROFILE: home };
|
|
delete env.GSD_TEST_MODE;
|
|
delete env.CLAUDE_CONFIG_DIR;
|
|
const r = spawnSync(process.execPath, [INSTALL_JS, '--gemini', '--uninstall', '--global'], {
|
|
cwd: home, env, encoding: 'utf8', timeout: INSTALL_TIMEOUT_MS,
|
|
});
|
|
|
|
assert.strictEqual(r.status, 1, 'must exit 1, not fall through to the uninstall dispatch');
|
|
assert.match(`${r.stderr || ''}`, /`--gemini --uninstall` is no longer available/, 'must guide manual cleanup');
|
|
assert.ok(fs.existsSync(path.join(home, '.claude', 'sentinel.txt')),
|
|
'the Claude install must NOT be uninstalled (the dispatch defaults empty selection to claude)');
|
|
});
|
|
|
|
test('--gemini co-selected with a valid runtime prints the notice AND still installs the other runtime', (t) => {
|
|
// Hermetic install via the repo harness (explicit --config-dir + isolated
|
|
// HOME). `--gemini` is added alongside a valid runtime (codex): the installer
|
|
// prints the notice but does NOT exit 1 (runMinimalInstall asserts status 0
|
|
// internally) and installs codex.
|
|
const { manifest, root, stderr } = runMinimalInstall({ runtime: 'codex', scope: 'global', extraArgs: ['--gemini'] });
|
|
t.after(() => cleanup(root));
|
|
|
|
assert.match(stderr, /sunset by Google on 2026-06-18/, 'the redirect notice still prints alongside the valid install');
|
|
assert.match(stderr, /--antigravity/);
|
|
assert.ok(manifest, 'the co-selected codex runtime must be installed (manifest written)');
|
|
});
|
|
|
|
test('control: an install WITHOUT --gemini does not print the sunset notice', (t) => {
|
|
const { root, stderr } = runMinimalInstall({ runtime: 'codex', scope: 'global' });
|
|
t.after(() => cleanup(root));
|
|
assert.doesNotMatch(stderr, /sunset by Google/, 'the notice must be conditional on --gemini');
|
|
});
|
|
});
|
|
|
|
describe('#1928 gemini removed from every runtime-name-policy surface', () => {
|
|
test('gemini aliases no longer canonicalize', () => {
|
|
for (const alias of ['gemini', 'gemini-cli', 'gemini-code']) {
|
|
assert.strictEqual(canonicalizeRuntimeName(alias), null, `${alias} must not resolve to a known runtime`);
|
|
}
|
|
});
|
|
|
|
test('gemini falls back on label / config-fragment / new-project surfaces', () => {
|
|
assert.strictEqual(getRuntimeLabel('gemini'), 'Claude Code', 'label table entry removed → fail-closed default');
|
|
assert.strictEqual(getGlobalConfigHomeFragment('gemini'), "'.claude'", 'config-home fragment removed → default');
|
|
assert.strictEqual(getRuntimeNewProjectCommand('gemini'), '/gsd-new-project', 'new-project override removed → default');
|
|
});
|
|
|
|
test('runtimeFlags has no isGemini and covers exactly the non-claude, CLI-installable registry runtimes (count-agnostic)', () => {
|
|
const flags = runtimeFlags('claude');
|
|
assert.ok(!('isGemini' in flags), 'isGemini flag must be gone');
|
|
// The flag set tracks the non-claude registry runtimes (one is<Runtime> per
|
|
// id), so adding a runtime updates the count automatically — no hand-pinned
|
|
// number that would break on the next runtime addition.
|
|
// #2103: registry runtimes with installSurface === 'none' (e.g. vscode —
|
|
// Marketplace/VSIX-distributed, never CLI-installed) have no --<rt> flag
|
|
// by design (see tests/runtime-flags.test.cjs's NON_INSTALLABLE_RUNTIMES)
|
|
// and are excluded from this count too.
|
|
const expectedNonClaudeCount = Object.keys(registry.runtimes)
|
|
.filter((id) => id !== 'claude' && registry.runtimes[id].runtime.installSurface !== 'none')
|
|
.length;
|
|
assert.strictEqual(Object.keys(flags).length, expectedNonClaudeCount,
|
|
'flag count must equal the non-claude, CLI-installable registry runtime count');
|
|
});
|
|
|
|
test('gemini no longer maps to GEMINI.md (defaults to AGENTS.md)', () => {
|
|
assert.strictEqual(getProjectInstructionFile('gemini'), 'AGENTS.md');
|
|
});
|
|
});
|
|
|
|
describe('#1928 Antigravity preserved (shared surface with the removed gemini runtime)', () => {
|
|
test('antigravity still resolves and keeps its GEMINI.md instruction file', () => {
|
|
assert.strictEqual(canonicalizeRuntimeName('antigravity'), 'antigravity');
|
|
assert.strictEqual(canonicalizeRuntimeName('antigravity-cli'), 'antigravity');
|
|
assert.strictEqual(getProjectInstructionFile('antigravity'), 'GEMINI.md',
|
|
'Antigravity CLI reads GEMINI.md as its contextFileName — this mapping must survive gemini removal');
|
|
assert.strictEqual(getRuntimeLabel('antigravity'), 'Antigravity');
|
|
});
|
|
|
|
test('the shared Gemini-backend tool vocabulary still powers Antigravity agent conversion', () => {
|
|
const input = ['---', 'name: gsd-x', 'description: d', 'tools: Read, Write, WebFetch, Skill', '---', '', 'body'].join('\n');
|
|
const toolsLine = convertClaudeAgentToAntigravityAgent(input).split('\n').find((l) => l.startsWith('tools:')) || '';
|
|
assert.ok(toolsLine.includes('read_file'), 'Read → read_file via the retained convertAntigravityToolName');
|
|
assert.ok(toolsLine.includes('write_file'), 'Write → write_file');
|
|
assert.ok(toolsLine.includes('web_fetch'), 'WebFetch → web_fetch');
|
|
assert.ok(!/\bskill\b/.test(toolsLine), 'Skill is still excluded (would be an invalid backend tool name)');
|
|
});
|
|
|
|
test('#4727 the rename is complete: no gemini-named alias survives alongside the antigravity-named exports', () => {
|
|
// Exports-shape check against the compiled module, not a text scan of source — #4727 renamed
|
|
// claudeToGeminiTools/convertGeminiToolName to claudeToAntigravityTools/convertAntigravityToolName
|
|
// in place; a partial rename would leave BOTH names live as two aliases for one concept, which
|
|
// is exactly the drift this epic exists to end.
|
|
const mod = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-artifact-conversion.cjs'));
|
|
|
|
assert.ok(
|
|
Object.prototype.hasOwnProperty.call(mod, 'claudeToAntigravityTools'),
|
|
'the renamed tool map must be exported under its new name',
|
|
);
|
|
assert.ok(
|
|
Object.prototype.hasOwnProperty.call(mod, 'convertAntigravityToolName'),
|
|
'the renamed conversion function must be exported under its new name',
|
|
);
|
|
assert.ok(
|
|
!Object.prototype.hasOwnProperty.call(mod, 'claudeToGeminiTools'),
|
|
'the retired gemini-named map must not still be exported — a surviving alias means the rename never finished',
|
|
);
|
|
assert.ok(
|
|
!Object.prototype.hasOwnProperty.call(mod, 'convertGeminiToolName'),
|
|
'the retired gemini-named function must not still be exported — a surviving alias means the rename never finished',
|
|
);
|
|
|
|
// The rename must be a pure identifier change: every value byte-identical, so an added OR
|
|
// removed key (not just a renamed export) fails this too.
|
|
assert.deepStrictEqual(mod.claudeToAntigravityTools, {
|
|
Read: 'read_file',
|
|
Write: 'write_file',
|
|
Edit: 'replace',
|
|
Bash: 'run_shell_command',
|
|
Glob: 'glob',
|
|
Grep: 'search_file_content',
|
|
WebSearch: 'google_web_search',
|
|
WebFetch: 'web_fetch',
|
|
TodoWrite: 'write_todos',
|
|
}, 'the tool map values are Gemini\'s built-in tool dialect, which Antigravity speaks — Google\'s '
|
|
+ 'contract, not GSD\'s to alter by renaming the map that carries it');
|
|
|
|
// In-set / out-of-set boundary pair: the excluded ids still return null individually...
|
|
for (const excluded of ['mcp__anything', 'Task', 'Agent', 'AskUserQuestion', 'ask_user', 'Skill', 'SlashCommand']) {
|
|
assert.strictEqual(
|
|
mod.convertAntigravityToolName(excluded),
|
|
null,
|
|
`${excluded} must still be excluded from the Antigravity tool dialect after the rename`,
|
|
);
|
|
}
|
|
// ...and an unmapped name still falls through to the lowercase default.
|
|
assert.strictEqual(
|
|
mod.convertAntigravityToolName('SomeOtherTool'),
|
|
'someothertool',
|
|
'an unmapped tool name must still lowercase-fallback after the rename',
|
|
);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* #4709 — the #1928 removal reached the installer and the runtime enum, but runtime-loaded
|
|
* workflow text kept MINTING the retired id: `RUNTIME="gemini"` from `$GEMINI_CONFIG_DIR`, a
|
|
* runtime selection menu offering "Gemini CLI.", a runtime->model-tier table row keyed `gemini`,
|
|
* and `config-set runtime gemini` examples.
|
|
*
|
|
* The name policy's unknown-id fallbacks are DELIBERATE and stay unchanged — see the
|
|
* 'gemini no longer maps to GEMINI.md (defaults to AGENTS.md)' test above, and
|
|
* src/runtime-name-policy.cts:220-222, which calls the label default "the always-safe default,
|
|
* fail-closed". This block removes the REACHABILITY instead: nothing shipped may mint an id the
|
|
* policy does not recognize.
|
|
*
|
|
* Every assertion is STRUCTURAL (the literal must be canonical / the runtime must exist as a
|
|
* catalog key), never "the string gemini is absent" — that string is load-bearing across
|
|
* Antigravity's real on-disk contract, which the final test pins.
|
|
*/
|
|
describe('#4709 no shipped surface mints a retired runtime id', () => {
|
|
/** Recursively collect every `.md` file under `dir` (missing dir -> []). */
|
|
function markdownFilesUnder(dir) {
|
|
if (!fs.existsSync(dir)) return [];
|
|
const out = [];
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) out.push(...markdownFilesUnder(full));
|
|
else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* The shipped, runtime-loaded markdown corpus this block governs. `agents/` is included
|
|
* deliberately: it ships runtime-loaded markdown too, including `.compact.md` variants, and
|
|
* leaving it out was a coverage gap an adversarial review caught.
|
|
*/
|
|
function shippedMarkdown() {
|
|
return [
|
|
...markdownFilesUnder(path.join(ROOT, 'gsd-core', 'workflows')),
|
|
...markdownFilesUnder(path.join(ROOT, 'commands')),
|
|
...markdownFilesUnder(path.join(ROOT, 'skills')),
|
|
...markdownFilesUnder(path.join(ROOT, 'agents')),
|
|
];
|
|
}
|
|
|
|
const relPath = (p) => path.relative(ROOT, p).split(path.sep).join('/');
|
|
const linesOf = (file) => fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
|
|
const SETTINGS_ADVANCED = path.join(ROOT, 'gsd-core', 'workflows', 'settings-advanced.md');
|
|
|
|
test('every RUNTIME= assignment in workflow text names a canonical runtime', () => {
|
|
// Bare-literal assignments only: RUNTIME=codex / RUNTIME="codex". A `$VAR`, `$(cmd)` or
|
|
// `<placeholder>` assignment is resolved at runtime and carries no id to validate here.
|
|
const ASSIGN = /\bRUNTIME=("?)([a-z][a-z0-9-]*)\1(?![\w-])/g;
|
|
const offenders = [];
|
|
|
|
for (const file of shippedMarkdown()) {
|
|
linesOf(file).forEach((line, i) => {
|
|
for (const m of line.matchAll(ASSIGN)) {
|
|
if (canonicalizeRuntimeName(m[2]) === null) {
|
|
offenders.push(`${relPath(file)}:${i + 1} mints RUNTIME=${m[2]}`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
assert.deepStrictEqual(offenders, [],
|
|
'shipped workflow text assigns a runtime id the name policy does not recognize. A '
|
|
+ 'non-canonical id does not fail loudly — it resolves to Claude Code defaults, so the '
|
|
+ `wrong config dir and instruction file are used silently. Offenders:\n ${offenders.join('\n ')}`);
|
|
});
|
|
|
|
test('the runtime tier table names only runtimes the model catalog defines', () => {
|
|
const known = new Set(Object.keys(catalog.runtimeTierDefaults));
|
|
const offenders = [];
|
|
|
|
// Rows of the runtime -> model-tier table: | `<id>` | `<opus>` | `<sonnet>` | `<haiku>` |
|
|
const ROW = /^\|\s*`([a-z][a-z0-9-]*)`\s*\|/;
|
|
linesOf(SETTINGS_ADVANCED).forEach((line, i) => {
|
|
const m = ROW.exec(line);
|
|
if (m && !known.has(m[1])) {
|
|
offenders.push(`${relPath(SETTINGS_ADVANCED)}:${i + 1} tables runtime \`${m[1]}\``);
|
|
}
|
|
});
|
|
|
|
assert.deepStrictEqual(offenders, [],
|
|
'the runtime->model-tier table documents built-in defaults for a runtime the model catalog '
|
|
+ 'has no entry for, so `config-set runtime <id>` would be ignored. The retired `gemini` '
|
|
+ 'row carried the three model IDs of the `google` PROVIDER preset — a provider axis '
|
|
+ `rendered as a runtime axis. Offenders:\n ${offenders.join('\n ')}`);
|
|
});
|
|
|
|
test('the runtime selection menu offers only canonical runtimes', () => {
|
|
const offenders = [];
|
|
|
|
// Scoped to RUNTIME menus by tracking the nearest preceding `question:`. The same file also
|
|
// carries a provider menu (anthropic / openai) and a budget menu (high / medium / low) whose
|
|
// labels are single lowercase tokens too; neither names a runtime, so validating those
|
|
// against the runtime policy would be a false positive, not extra rigor.
|
|
const QUESTION = /^\s*question:\s*"(.*)"\s*,?\s*$/;
|
|
const OPTION = /\{\s*label:\s*"([a-z][a-z0-9-]*)"\s*,\s*description:/;
|
|
let inRuntimeMenu = false;
|
|
|
|
linesOf(SETTINGS_ADVANCED).forEach((line, i) => {
|
|
const q = QUESTION.exec(line);
|
|
if (q) {
|
|
inRuntimeMenu = /runtime/i.test(q[1]);
|
|
return;
|
|
}
|
|
if (!inRuntimeMenu) return;
|
|
const m = OPTION.exec(line);
|
|
if (m && canonicalizeRuntimeName(m[1]) === null) {
|
|
offenders.push(`${relPath(SETTINGS_ADVANCED)}:${i + 1} offers \`${m[1]}\``);
|
|
}
|
|
});
|
|
|
|
assert.deepStrictEqual(offenders, [],
|
|
'a runtime selection menu offers a runtime GSD does not support — selecting it writes a '
|
|
+ `config value that silently resolves to Claude Code. Offenders:\n ${offenders.join('\n ')}`);
|
|
});
|
|
|
|
test('documented config examples name only canonical runtimes', () => {
|
|
const offenders = [];
|
|
const SET_RUNTIME = /config-set\s+runtime\s+([a-z][a-z0-9-]*)/g;
|
|
const OVERRIDE = /model_profile_overrides\.([a-z][a-z0-9-]*)\./g;
|
|
|
|
for (const file of shippedMarkdown()) {
|
|
linesOf(file).forEach((line, i) => {
|
|
for (const m of line.matchAll(SET_RUNTIME)) {
|
|
if (canonicalizeRuntimeName(m[1]) === null) {
|
|
offenders.push(`${relPath(file)}:${i + 1} \`config-set runtime ${m[1]}\``);
|
|
}
|
|
}
|
|
for (const m of line.matchAll(OVERRIDE)) {
|
|
if (canonicalizeRuntimeName(m[1]) === null) {
|
|
offenders.push(`${relPath(file)}:${i + 1} \`model_profile_overrides.${m[1]}\``);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
assert.deepStrictEqual(offenders, [],
|
|
'a documented example sets a runtime id the name policy does not recognize; a user who '
|
|
+ `copies it lands on Claude Code defaults. Offenders:\n ${offenders.join('\n ')}`);
|
|
});
|
|
|
|
test("Antigravity's Gemini-family descriptor contract is preserved", () => {
|
|
// Negative space for every test above: Antigravity's real on-disk contract IS Google's
|
|
// Gemini surface, so an over-broad gemini -> antigravity replacement must fail HERE rather
|
|
// than ship. Asserted against the DESCRIPTOR, never a resolved path — getGlobalConfigDir()
|
|
// reads $ANTIGRAVITY_CONFIG_DIR and the real $HOME, which is the #4312 defect class.
|
|
const agy = JSON.parse(
|
|
fs.readFileSync(path.join(ROOT, 'capabilities', 'antigravity', 'capability.json'), 'utf8'),
|
|
);
|
|
assert.strictEqual(agy.runtime.configHome.parent, '.gemini',
|
|
"Antigravity's config home is nested under ~/.gemini");
|
|
assert.strictEqual(agy.runtime.configHome.name, 'antigravity');
|
|
assert.strictEqual(agy.runtime.hookEvents, 'gemini',
|
|
'Antigravity speaks the Gemini hook-event dialect');
|
|
assert.strictEqual(agy.runtime.hostBehaviors.projectInstructionFile, 'GEMINI.md');
|
|
for (const kind of agy.runtime.artifactLayout.global) {
|
|
assert.strictEqual(kind.home, '.gemini/config',
|
|
'global skills/agents install to ~/.gemini/config, the dir agy scans (#3738)');
|
|
}
|
|
|
|
assert.ok(Object.prototype.hasOwnProperty.call(catalog.runtimeTierDefaults, 'antigravity'),
|
|
'antigravity must remain a model-catalog runtime');
|
|
|
|
// The three model IDs the stale `gemini` table row carried belong to the google PROVIDER
|
|
// preset and must survive — they name real Google models, not a GSD runtime.
|
|
const google = JSON.stringify(catalog.providerPresets.google);
|
|
for (const model of ['gemini-3.1-pro-preview', 'gemini-3-flash', 'gemini-2.5-flash-lite']) {
|
|
assert.ok(google.includes(model), `google provider preset must still offer ${model}`);
|
|
}
|
|
});
|
|
|
|
test('PR template runtime checklists name only supported runtimes', () => {
|
|
// #1928's follow-up dropped Gemini CLI from .github/ISSUE_TEMPLATE/*.yml but missed the PR
|
|
// templates, which kept offering it under "Runtimes tested" -- a contributor-facing surface
|
|
// still advertising a retired runtime two releases later. Labels here are DISPLAY names
|
|
// ("Claude Code", not "claude"), so they are checked against the label table, not the id set.
|
|
const labels = new Set(
|
|
Object.keys(registry.runtimes).map((id) => getRuntimeLabel(id)),
|
|
);
|
|
// Non-runtime checklist entries that legitimately appear in the same list.
|
|
const NON_RUNTIME = /^(Other:|N\/A\b)/;
|
|
const offenders = [];
|
|
|
|
const templateDir = path.join(ROOT, '.github', 'PULL_REQUEST_TEMPLATE');
|
|
for (const name of fs.readdirSync(templateDir).filter((f) => f.endsWith('.md'))) {
|
|
const file = path.join(templateDir, name);
|
|
const fileLines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
let inRuntimeSection = false;
|
|
fileLines.forEach((line, i) => {
|
|
if (/^#+\s*Runtimes tested/i.test(line)) {
|
|
inRuntimeSection = true;
|
|
return;
|
|
}
|
|
// The section ends at the next heading or horizontal rule.
|
|
if (inRuntimeSection && /^(#+\s|---\s*$)/.test(line)) {
|
|
inRuntimeSection = false;
|
|
return;
|
|
}
|
|
if (!inRuntimeSection) return;
|
|
const m = /^\s*-\s*\[\s*\]\s*(.+?)\s*$/.exec(line);
|
|
if (!m) return;
|
|
const label = m[1];
|
|
if (NON_RUNTIME.test(label)) return;
|
|
if (!labels.has(label)) {
|
|
offenders.push(`.github/PULL_REQUEST_TEMPLATE/${name}:${i + 1} offers "${label}"`);
|
|
}
|
|
});
|
|
}
|
|
|
|
assert.deepStrictEqual(offenders, [],
|
|
'a PR template asks contributors which runtime they tested and lists one GSD does not '
|
|
+ 'support. Labels must match the runtime label table (src/runtime-name-policy.cts '
|
|
+ `RUNTIME_LABELS), so a retired runtime cannot linger here. Offenders:\n ${offenders.join('\n ')}`);
|
|
});
|
|
|
|
/**
|
|
* #4709 Phase 3 — the Gemini CLI reviewer lane is retired.
|
|
*
|
|
* #1928 removed the gemini RUNTIME in 1.8.0 after Google sunset Gemini CLI on 2026-06-18. The
|
|
* reviewer lane was re-created afterwards by the reviewer-lane-as-manifest-data work (6a9babda69,
|
|
* #2798/#2837) — per the maintainer that re-creation was an error in that buildout, not a
|
|
* decision, so retiring it corrects a mistake and needs no ADR-2782 amendment.
|
|
*
|
|
* The lane spawned `gemini {{model}} -p -`, a binary Google no longer serves for the
|
|
* free/Pro/Ultra tiers that ARE GSD's audience.
|
|
*
|
|
* Every assertion below is STRUCTURAL — a declared lane, an owned config key, a capability count.
|
|
* None asserts that the string "gemini" is absent, because that string is load-bearing across
|
|
* Antigravity's real on-disk contract (~/.gemini/antigravity, ~/.gemini/config, hookEvents
|
|
* "gemini", GEMINI.md) and across Google's own model IDs. The Antigravity block below is the
|
|
* negative space that keeps this removal from overreaching.
|
|
*/
|
|
describe('#4709 the Gemini CLI reviewer lane is retired', () => {
|
|
const reviewerIds = () => Object.keys(registry.capabilities)
|
|
.filter((id) => registry.capabilities[id] && registry.capabilities[id].reviewer);
|
|
|
|
test('capabilities/gemini/ no longer exists', () => {
|
|
assert.strictEqual(
|
|
fs.existsSync(path.join(ROOT, 'capabilities', 'gemini')),
|
|
false,
|
|
'the gemini capability directory must be deleted, not emptied',
|
|
);
|
|
});
|
|
|
|
test('no capability declares a gemini reviewer lane', () => {
|
|
const offenders = reviewerIds().filter((id) => {
|
|
const rev = registry.capabilities[id].reviewer;
|
|
return id === 'gemini' || rev.slug === 'gemini' || (rev.flags || []).includes('--gemini');
|
|
});
|
|
assert.deepStrictEqual(
|
|
offenders,
|
|
[],
|
|
'a reviewer lane still resolves for the retired Gemini CLI; --gemini would spawn a binary '
|
|
+ `Google stopped serving on 2026-06-18. Offenders: ${offenders.join(', ')}`,
|
|
);
|
|
});
|
|
|
|
test('no config key is owned for the retired lane', () => {
|
|
const offenders = Object.keys(registry.configKeys).filter((k) => /\.gemini$/.test(k));
|
|
assert.deepStrictEqual(
|
|
offenders,
|
|
[],
|
|
'the retired lane still owns config keys, so `gsd config-set` would accept settings for a '
|
|
+ `lane that cannot run. Offenders:\n ${offenders.join('\n ')}`,
|
|
);
|
|
});
|
|
|
|
test('exactly 11 reviewer lanes remain', () => {
|
|
// Counted from the registry, not hardcoded per-name, so adding a 12th lane later cannot
|
|
// silently re-admit gemini under cover of the count still "looking right".
|
|
const ids = reviewerIds().sort();
|
|
assert.strictEqual(
|
|
ids.length,
|
|
11,
|
|
`expected 11 reviewer lanes after retiring gemini, got ${ids.length}: ${ids.join(', ')}`,
|
|
);
|
|
assert.ok(!ids.includes('gemini'), 'gemini must not be among them');
|
|
});
|
|
|
|
test("Antigravity's reviewer lane is untouched (negative space)", () => {
|
|
const agy = registry.capabilities.antigravity;
|
|
assert.ok(agy && agy.reviewer, 'antigravity must still declare a reviewer lane');
|
|
assert.strictEqual(agy.reviewer.slug, 'antigravity');
|
|
for (const flag of ['--antigravity', '--agy']) {
|
|
assert.ok(
|
|
(agy.reviewer.flags || []).includes(flag),
|
|
`antigravity must keep its ${flag} flag`,
|
|
);
|
|
}
|
|
// Its own keys survive, including the deliberately `agy`-suffixed model key.
|
|
for (const key of [
|
|
'review.models.agy',
|
|
'review.timeouts.antigravity',
|
|
'review.max_prompt_tokens_per_reviewer.antigravity',
|
|
]) {
|
|
assert.ok(
|
|
Object.prototype.hasOwnProperty.call(registry.configKeys, key),
|
|
`antigravity must still own ${key}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('the other ten lanes are untouched (negative space)', () => {
|
|
const expected = [
|
|
'antigravity', 'claude', 'coderabbit', 'codex', 'cursor',
|
|
'kimi-code', 'llama-cpp', 'lm-studio', 'ollama', 'opencode', 'qwen',
|
|
];
|
|
assert.deepStrictEqual(
|
|
reviewerIds().sort(),
|
|
expected,
|
|
'retiring gemini must remove exactly one lane and disturb no other',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* #4728 — localized docs and runtime-loaded workflow prose still surface the retired Gemini CLI
|
|
* runtime as a selectable option, outside the English `how-to`/`ARCHITECTURE.md` surfaces #4709
|
|
* already covers.
|
|
*
|
|
* As with #4709, every assertion here is STRUCTURAL: a heading position, a table's first cell, or
|
|
* a captured runtime-example parenthetical — never "the string gemini is absent". `gemini` is
|
|
* load-bearing across Antigravity's real on-disk contract (~/.gemini/antigravity, ~/.gemini/config,
|
|
* GEMINI.md, hookEvents "gemini", GEMINI_API_KEY, every gemini-* model id), so a bare-string
|
|
* assertion would be wrong and would fail on correct code. The PRESERVE block below is the
|
|
* over-reach guard: it is what fails first if a "helpful" blanket gemini->antigravity sweep lands
|
|
* instead of the scoped removal this issue asks for.
|
|
*/
|
|
describe('#4728 Gemini CLI prose retired from localized docs and runtime-loaded workflows', () => {
|
|
const LOCALES = ['ja-JP', 'ko-KR', 'pt-BR', 'zh-CN'];
|
|
const linesOf = (file) => fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
const relPath = (p) => path.relative(ROOT, p).split(path.sep).join('/');
|
|
|
|
test('no locale install-on-your-runtime.md has a Gemini CLI ### heading', () => {
|
|
// Heading-scoped (### only) so Antigravity's own ~/.gemini/... paths documented in the same
|
|
// file cannot trip this — a bare substring match on "Gemini CLI" would be too broad here.
|
|
const HEADING = /^###\s+Gemini CLI\s*$/m;
|
|
const offenders = LOCALES
|
|
.map((locale) => path.join(ROOT, 'docs', locale, 'how-to', 'install-on-your-runtime.md'))
|
|
.filter((file) => HEADING.test(fs.readFileSync(file, 'utf8')))
|
|
.map(relPath);
|
|
|
|
assert.deepStrictEqual(offenders, [],
|
|
'a localized how-to still documents Gemini CLI as an installable runtime under its own '
|
|
+ `heading, after #1928 retired the runtime. Offenders:\n ${offenders.join('\n ')}`);
|
|
});
|
|
|
|
test('no locale ARCHITECTURE.md tables a Gemini CLI row', () => {
|
|
// First-cell-scoped so the Antigravity row's ~/.gemini/antigravity cells are untouched.
|
|
const ROW = /^\|\s*Gemini CLI\s*\|/m;
|
|
const offenders = LOCALES
|
|
.map((locale) => path.join(ROOT, 'docs', locale, 'ARCHITECTURE.md'))
|
|
.filter((file) => ROW.test(fs.readFileSync(file, 'utf8')))
|
|
.map(relPath);
|
|
|
|
assert.deepStrictEqual(offenders, [],
|
|
'a localized ARCHITECTURE.md still tables Gemini CLI as a runtime row, after #1928 retired '
|
|
+ `the runtime. Offenders:\n ${offenders.join('\n ')}`);
|
|
});
|
|
|
|
test('no locale USER-GUIDE.md tables a Gemini CLI row', () => {
|
|
const ROW = /^\|\s*Gemini CLI\s*\|/m;
|
|
const offenders = LOCALES
|
|
.map((locale) => path.join(ROOT, 'docs', locale, 'USER-GUIDE.md'))
|
|
.filter((file) => ROW.test(fs.readFileSync(file, 'utf8')))
|
|
.map(relPath);
|
|
|
|
assert.deepStrictEqual(offenders, [],
|
|
'a localized USER-GUIDE.md still tables Gemini CLI as a runtime row, after #1928 retired the '
|
|
+ `runtime. Offenders:\n ${offenders.join('\n ')}`);
|
|
});
|
|
|
|
test('no runtime-loaded workflow file names the retired Gemini runtime', () => {
|
|
// Walk gsd-core/workflows/ recursively (this MUST cover nested dirs like
|
|
// new-project/steps/, where #4728's auto-mode-config.md defect lived) and assert no `.md`
|
|
// file contains a case-sensitive standalone `Gemini` token, i.e. no match for /\bGemini\b/.
|
|
//
|
|
// Why a bare `/\bGemini\b/` is the right predicate, and not a narrower parenthetical/heading
|
|
// scoped one: every LEGITIMATE gemini reference in this directory is spelled differently and
|
|
// therefore cannot collide with this assertion —
|
|
// - Antigravity's config paths are lowercase with a slash: ~/.gemini/antigravity,
|
|
// ~/.gemini/config, .gemini/antigravity-ide
|
|
// - Google model ids are lowercase and hyphenated: gemini-3.1-pro-preview,
|
|
// gemini-2.5-flash-lite
|
|
// - env vars are uppercase: GEMINI_CONFIG_DIR, GEMINI_SESSION_ID, GEMINI_API_KEY
|
|
// - the generated runtime-launcher preamble only ever uses GEMINI_CONFIG_DIR / $HOME/.gemini
|
|
// So a bare capitalised `Gemini` anywhere under gsd-core/workflows/ can only mean the retired
|
|
// RUNTIME is being named — which is exactly the defect this test exists to catch. These files
|
|
// are runtime-loaded (read by the agent at execution time), so a stale "Gemini" mention here
|
|
// actively steers execution toward a retired runtime, not merely misinforms a reader.
|
|
const WORKFLOWS_ROOT = path.join(ROOT, 'gsd-core', 'workflows');
|
|
const GEMINI_TOKEN = /\bGemini\b/;
|
|
// Two allowlisted exceptions, both matched by line CONTENT (never line number, so neither can
|
|
// silently drift if the file is edited above the matched line):
|
|
// 1. reapply-patches.md's historical note about where a pre-#1928 Gemini CLI install used
|
|
// to place patches.
|
|
// 2. settings-advanced.md's Section 8 "Known provider" menu option. This predicate forbids
|
|
// `Gemini` naming the retired RUNTIME axis; it does NOT forbid `Gemini` naming the
|
|
// PROVIDER / MODEL-FAMILY axis. Section 8 ("Model Policy") is explicitly scoped to model
|
|
// selection, independent of which runtime is installed, and its own intro says so. The
|
|
// "Known provider" option lists Gemini alongside Claude/OpenAI/Qwen as a model provider —
|
|
// the same axis as the lowercase `gemini-*` model ids used elsewhere in this file — and
|
|
// Antigravity runs on that provider's models. That is the same runtime-vs-provider
|
|
// taxonomy the whole #4709 epic rests on, so this line is correct as written and must
|
|
// keep working, not get rewritten to dodge the predicate.
|
|
const isAllowlistedLegacyLine = (file, line) =>
|
|
(relPath(file) === 'gsd-core/workflows/reapply-patches.md'
|
|
&& line.includes('Legacy:')
|
|
&& line.includes('pre-#1928'))
|
|
|| (relPath(file) === 'gsd-core/workflows/settings-advanced.md'
|
|
&& line.includes('Known provider'));
|
|
|
|
/** Recursively collect every `.md` file under `dir`. */
|
|
function markdownFilesUnder(dir) {
|
|
const out = [];
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
out.push(...markdownFilesUnder(full));
|
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
out.push(full);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const files = markdownFilesUnder(WORKFLOWS_ROOT);
|
|
// Guard against an empty or failed walk making this test pass vacuously.
|
|
assert.ok(files.length >= 50,
|
|
`expected at least 50 .md files under gsd-core/workflows/, found ${files.length} — the `
|
|
+ 'recursive walk may be broken');
|
|
|
|
const offenders = [];
|
|
for (const file of files) {
|
|
linesOf(file).forEach((line, i) => {
|
|
if (isAllowlistedLegacyLine(file, line)) return;
|
|
if (GEMINI_TOKEN.test(line)) {
|
|
offenders.push(`${relPath(file)}:${i + 1} names Gemini: "${line.trim()}"`);
|
|
}
|
|
});
|
|
}
|
|
|
|
assert.deepStrictEqual(offenders, [],
|
|
'a runtime-loaded workflow file under gsd-core/workflows/ still names the retired Gemini '
|
|
+ 'runtime, steering the agent (these files are read at execution time, not just by a '
|
|
+ `reader) toward a retired runtime. Offenders:\n ${offenders.join('\n ')}`);
|
|
});
|
|
|
|
test('PRESERVE: Antigravity survives in every surface the removal above touches (over-reach guard)', () => {
|
|
// These four assertions are what fails first if someone "fixes" #4728 with a blanket
|
|
// gemini->antigravity string sweep instead of the scoped, structural removal above.
|
|
for (const locale of LOCALES) {
|
|
const installGuide = fs.readFileSync(
|
|
path.join(ROOT, 'docs', locale, 'how-to', 'install-on-your-runtime.md'), 'utf8',
|
|
);
|
|
assert.match(installGuide, /^###\s+Antigravity\s*$/m,
|
|
`docs/${locale}/how-to/install-on-your-runtime.md must still document Antigravity under its own heading`);
|
|
|
|
const architecture = fs.readFileSync(path.join(ROOT, 'docs', locale, 'ARCHITECTURE.md'), 'utf8');
|
|
assert.ok(architecture.includes('~/.gemini/antigravity'),
|
|
`docs/${locale}/ARCHITECTURE.md must still document Antigravity's ~/.gemini/antigravity config home`);
|
|
|
|
const configuration = fs.readFileSync(path.join(ROOT, 'docs', locale, 'CONFIGURATION.md'), 'utf8');
|
|
assert.ok(configuration.includes('GEMINI_API_KEY'),
|
|
`docs/${locale}/CONFIGURATION.md must still document GEMINI_API_KEY`);
|
|
|
|
const contextMonitor = fs.readFileSync(path.join(ROOT, 'docs', locale, 'context-monitor.md'), 'utf8');
|
|
assert.ok(contextMonitor.includes('AfterTool'),
|
|
`docs/${locale}/context-monitor.md must still document AfterTool, the hook dialect Antigravity `
|
|
+ 'inherits — re-attributed on gemini removal, never deleted');
|
|
}
|
|
});
|
|
|
|
test('MODEL AXIS: Gemini 2.5 Pro asymmetry across locale ARCHITECTURE.md is a measured fact, not an assumption', () => {
|
|
// Measured on 2026-09-14: ko-KR/pt-BR/zh-CN mention Gemini 2.5 Pro on the model axis (distinct
|
|
// from the retired RUNTIME axis covered above); ja-JP genuinely does not. Both sides are
|
|
// asserted so a later "helpful" uniform patch that adds the mention to ja-JP — treating the
|
|
// asymmetry as an oversight rather than a measured divergence — fails here.
|
|
for (const locale of ['ko-KR', 'pt-BR', 'zh-CN']) {
|
|
const text = fs.readFileSync(path.join(ROOT, 'docs', locale, 'ARCHITECTURE.md'), 'utf8');
|
|
assert.ok(text.includes('Gemini 2.5 Pro'),
|
|
`docs/${locale}/ARCHITECTURE.md must still mention Gemini 2.5 Pro on the model axis`);
|
|
}
|
|
|
|
const jaText = fs.readFileSync(path.join(ROOT, 'docs', 'ja-JP', 'ARCHITECTURE.md'), 'utf8');
|
|
assert.ok(!jaText.includes('Gemini 2.5 Pro'),
|
|
'docs/ja-JP/ARCHITECTURE.md does not mention Gemini 2.5 Pro — this locale genuinely diverged '
|
|
+ 'from the other three and must not be uniformly patched to match them');
|
|
});
|
|
});
|