Files
msd-core/tests/issue-775-gemini-extension.test.cjs
Tom Boucher a3aa0ae142 feat(#775): ship a gemini-extension.json extension package (#818)
Add a Gemini CLI extension package so users can install, update, and
remove GSD through Gemini's own extension lifecycle and have it appear in
`gemini extensions list`:

  gemini extensions install https://github.com/open-gsd/gsd-core
  gemini extensions update gsd-core
  gemini extensions uninstall gsd-core
  gemini extensions link /path/to/gsd-core   # dev

This mirrors the additive Claude Code plugin manifest (#766): a thin,
version-stamped manifest enforced by an in-repo drift test. The extension
ships the context-file payload (GEMINI.md), loaded into every Gemini
session; slash-command/agent/hook TOML projection into the extension is a
documented follow-up. The manual `npx gsd-core --gemini` installer (which
provides the /gsd:* commands) is unchanged — purely additive, no breaking
change.

- gemini-extension.json: name=binName, version tracks package.json,
  description, contextFileName=GEMINI.md (minimal; no mcpServers — gsd
  ships no MCP server)
- GEMINI.md: Gemini-session context payload
- package.json: add both artifacts to files[] so they publish
- CONTEXT.md: add "Gemini Extension Package" glossary entry
- docs: USER-GUIDE + install-on-your-runtime how-to
- tests/issue-775-gemini-extension.test.cjs: manifest validity, version
  parity with package.json, contextFileName existence, files[] publication

Closes #775

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:54:08 -04:00

120 lines
5.2 KiB
JavaScript

'use strict';
/**
* Regression tests for issue #775: additive Gemini CLI extension manifest.
*
* Asserts structural and semantic correctness of the Gemini Extension Package:
* gemini-extension.json — extension manifest (consumed by
* `gemini extensions install <git-url>`)
* GEMINI.md — the extension's context-file payload, referenced
* by the manifest's `contextFileName` field.
*
* This mirrors tests/issue-766-plugin-manifest.test.cjs (the parallel Claude
* Code plugin manifest) — the Gemini extension is the same artifact-surface
* projection onto Gemini CLI's package contract.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const identity = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'package-identity.cjs'));
const pkg = require(path.join(ROOT, 'package.json'));
const MANIFEST_PATH = path.join(ROOT, 'gemini-extension.json');
// ─── Section A: gemini-extension.json ────────────────────────────────────────
describe('A: gemini-extension.json', () => {
let manifest;
test('exists and is valid JSON', () => {
assert.ok(fs.existsSync(MANIFEST_PATH), 'gemini-extension.json must exist at repo root');
const raw = fs.readFileSync(MANIFEST_PATH, 'utf-8');
manifest = JSON.parse(raw); // throws on invalid JSON
assert.ok(typeof manifest === 'object' && manifest !== null, 'manifest must be a JSON object');
});
test('name equals identity.binName ("gsd-core")', (t) => {
if (!manifest) { t.skip('manifest could not be parsed'); return; }
assert.equal(manifest.name, identity.binName, `name should be "${identity.binName}"`);
});
test('name is lowercase/dashes only (Gemini extension naming rule)', (t) => {
if (!manifest) { t.skip('manifest could not be parsed'); return; }
// Gemini reference: "lowercase or numbers and use dashes instead of
// underscores or spaces".
assert.match(
manifest.name,
/^[a-z0-9]+(?:-[a-z0-9]+)*$/,
'name must be lowercase letters/numbers with dashes (no underscores, spaces, or uppercase)'
);
});
test('version is a non-empty string matching package.json version', (t) => {
if (!manifest) { t.skip('manifest could not be parsed'); return; }
assert.equal(
manifest.version,
pkg.version,
`gemini-extension.json version (${manifest.version}) must match package.json version (${pkg.version}). ` +
`When bumping the package version, update gemini-extension.json \`version\` to match — ` +
`Gemini CLI's \`gemini extensions update\` keys off the manifest version field. (#775)`
);
});
test('description is a non-empty string (required by Gemini manifest schema)', (t) => {
if (!manifest) { t.skip('manifest could not be parsed'); return; }
assert.ok(
typeof manifest.description === 'string' && manifest.description.trim().length > 0,
'description must be a non-empty string'
);
});
test('contextFileName points to an existing repo-root file', (t) => {
if (!manifest) { t.skip('manifest could not be parsed'); return; }
assert.equal(manifest.contextFileName, 'GEMINI.md', 'contextFileName must be "GEMINI.md"');
const ctx = path.join(ROOT, manifest.contextFileName);
assert.ok(fs.existsSync(ctx), `context file must exist on disk: ${ctx}`);
const body = fs.readFileSync(ctx, 'utf-8');
assert.ok(body.trim().length > 0, 'context file must be non-empty');
});
test('only declares schema-known top-level keys', (t) => {
if (!manifest) { t.skip('manifest could not be parsed'); return; }
// Per Gemini extension reference. We intentionally ship the minimal
// context-loading subset; this guards against typos / unknown keys that
// would fail `gemini extensions install` manifest validation.
const ALLOWED = new Set([
'name', 'version', 'description', 'contextFileName',
'mcpServers', 'excludeTools', 'migratedTo', 'plan', 'settings', 'themes',
]);
for (const key of Object.keys(manifest)) {
assert.ok(ALLOWED.has(key), `Unknown top-level manifest key "${key}" is not in the Gemini extension schema`);
}
});
test('does not declare mcpServers (gsd-core ships no MCP server)', (t) => {
if (!manifest) { t.skip('manifest could not be parsed'); return; }
assert.ok(
!Object.prototype.hasOwnProperty.call(manifest, 'mcpServers'),
'manifest must NOT declare mcpServers — gsd-core has no MCP server'
);
});
});
// ─── Section B: package publication ──────────────────────────────────────────
describe('B: package.json publication', () => {
test('files[] includes gemini-extension.json and GEMINI.md', () => {
const files = pkg.files || [];
for (const required of ['gemini-extension.json', 'GEMINI.md']) {
assert.ok(
files.includes(required),
`package.json "files" must include "${required}" so it ships to npm consumers`
);
}
});
});