Files
get-shit-done/tests/antigravity-install.test.cjs
Tom Boucher 2703422be8 refactor(tests): standardize to node:assert/strict and t.after() per CONTRIBUTING.md (#1675)
* refactor(tests): standardize to node:assert/strict and t.after() per CONTRIBUTING.md

- Replace require('node:assert') with require('node:assert/strict') across
  all 73 test files to enforce strict equality (no type coercion)
- Replace try/finally cleanup blocks with t.after() hooks in core.test.cjs
  and hooks-opt-in.test.cjs per the test lifecycle standards
- Utility functions in codex-config and security-scan retain try/finally
  as that is appropriate for per-function resource guards, not lifecycle hooks

Closes #1674

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* perf(tests): add --test-concurrency=4 to test runner for parallel file execution

Node.js --test-concurrency controls how many test files run as parallel child
processes. Set to 4 by default, configurable via TEST_CONCURRENCY env var.
Fixes tests at a known level rather than inheriting os.availableParallelism()
which varies across CI environments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(security): allowlist verify.test.cjs in prompt-injection scanner

tests/verify.test.cjs uses <human>...</human> as GSD phase task-type
XML (meaning "a human should verify this step"), which matches the
scanner's fake-message-boundary pattern for LLM APIs. This is a
false positive — add it to the allowlist alongside the other test files
that legitimately contain injection-adjacent patterns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 14:29:03 -04:00

427 lines
17 KiB
JavaScript

/**
* GSD Tools Tests - Antigravity Install Plumbing
*
* Tests for Antigravity runtime directory resolution, config paths,
* content conversion functions, and integration with the multi-runtime installer.
*/
process.env.GSD_TEST_MODE = '1';
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const path = require('path');
const os = require('os');
const fs = require('fs');
const { createTempDir, cleanup } = require('./helpers.cjs');
const {
getDirName,
getGlobalDir,
getConfigDirFromHome,
convertClaudeToAntigravityContent,
convertClaudeCommandToAntigravitySkill,
convertClaudeAgentToAntigravityAgent,
copyCommandsAsAntigravitySkills,
writeManifest,
} = require('../bin/install.js');
// ─── getDirName ─────────────────────────────────────────────────────────────────
describe('getDirName (Antigravity)', () => {
test('returns .agent for antigravity', () => {
assert.strictEqual(getDirName('antigravity'), '.agent');
});
test('does not break existing runtimes', () => {
assert.strictEqual(getDirName('claude'), '.claude');
assert.strictEqual(getDirName('opencode'), '.opencode');
assert.strictEqual(getDirName('gemini'), '.gemini');
assert.strictEqual(getDirName('kilo'), '.kilo');
assert.strictEqual(getDirName('codex'), '.codex');
assert.strictEqual(getDirName('copilot'), '.github');
});
});
// ─── getGlobalDir ───────────────────────────────────────────────────────────────
describe('getGlobalDir (Antigravity)', () => {
let savedEnv;
beforeEach(() => {
savedEnv = process.env.ANTIGRAVITY_CONFIG_DIR;
delete process.env.ANTIGRAVITY_CONFIG_DIR;
});
afterEach(() => {
if (savedEnv !== undefined) {
process.env.ANTIGRAVITY_CONFIG_DIR = savedEnv;
} else {
delete process.env.ANTIGRAVITY_CONFIG_DIR;
}
});
test('returns ~/.gemini/antigravity by default', () => {
const result = getGlobalDir('antigravity');
assert.strictEqual(result, path.join(os.homedir(), '.gemini', 'antigravity'));
});
test('respects ANTIGRAVITY_CONFIG_DIR env var', () => {
const customDir = path.join(os.homedir(), 'custom-ag');
process.env.ANTIGRAVITY_CONFIG_DIR = customDir;
const result = getGlobalDir('antigravity');
assert.strictEqual(result, customDir);
});
test('explicit config-dir overrides env var', () => {
process.env.ANTIGRAVITY_CONFIG_DIR = path.join(os.homedir(), 'from-env');
const explicit = path.join(os.homedir(), 'explicit-ag');
const result = getGlobalDir('antigravity', explicit);
assert.strictEqual(result, explicit);
});
test('does not change Claude Code global dir', () => {
assert.strictEqual(getGlobalDir('claude'), path.join(os.homedir(), '.claude'));
});
});
// ─── getConfigDirFromHome ───────────────────────────────────────────────────────
describe('getConfigDirFromHome (Antigravity)', () => {
test('returns .agent for local installs', () => {
assert.strictEqual(getConfigDirFromHome('antigravity', false), "'.agent'");
});
test('returns .gemini, antigravity for global installs', () => {
assert.strictEqual(getConfigDirFromHome('antigravity', true), "'.gemini', 'antigravity'");
});
test('does not change other runtimes', () => {
assert.strictEqual(getConfigDirFromHome('claude', true), "'.claude'");
assert.strictEqual(getConfigDirFromHome('gemini', true), "'.gemini'");
assert.strictEqual(getConfigDirFromHome('kilo', true), "'.config', 'kilo'");
assert.strictEqual(getConfigDirFromHome('copilot', true), "'.copilot'");
});
});
// ─── convertClaudeToAntigravityContent ─────────────────────────────────────────
describe('convertClaudeToAntigravityContent', () => {
describe('global install path replacements', () => {
test('replaces ~/. claude/ with ~/.gemini/antigravity/', () => {
const input = 'See ~/.claude/get-shit-done/workflows/';
const result = convertClaudeToAntigravityContent(input, true);
assert.ok(result.includes('~/.gemini/antigravity/get-shit-done/workflows/'), result);
assert.ok(!result.includes('~/.claude/'), result);
});
test('replaces $HOME/.claude/ with $HOME/.gemini/antigravity/', () => {
const input = 'path.join($HOME/.claude/get-shit-done)';
const result = convertClaudeToAntigravityContent(input, true);
assert.ok(result.includes('$HOME/.gemini/antigravity/'), result);
assert.ok(!result.includes('$HOME/.claude/'), result);
});
});
describe('local install path replacements', () => {
test('replaces ~/.claude/ with .agent/ for local installs', () => {
const input = 'See ~/.claude/get-shit-done/';
const result = convertClaudeToAntigravityContent(input, false);
assert.ok(result.includes('.agent/get-shit-done/'), result);
assert.ok(!result.includes('~/.claude/'), result);
});
test('replaces ./.claude/ with ./.agent/', () => {
const input = 'path ./.claude/hooks/gsd-check-update.js';
const result = convertClaudeToAntigravityContent(input, false);
assert.ok(result.includes('./.agent/hooks/'), result);
assert.ok(!result.includes('./.claude/'), result);
});
test('replaces .claude/ with .agent/', () => {
const input = 'node .claude/hooks/gsd-statusline.js';
const result = convertClaudeToAntigravityContent(input, false);
assert.ok(result.includes('.agent/hooks/gsd-statusline.js'), result);
assert.ok(!result.includes('.claude/'), result);
});
});
describe('command name conversion', () => {
test('converts /gsd:command to /gsd-command', () => {
const input = 'Run /gsd:new-project to start';
const result = convertClaudeToAntigravityContent(input, true);
assert.ok(result.includes('/gsd-new-project'), result);
assert.ok(!result.includes('gsd:'), result);
});
test('converts all gsd: references', () => {
const input = '/gsd:plan-phase and /gsd:execute-phase';
const result = convertClaudeToAntigravityContent(input, false);
assert.ok(result.includes('/gsd-plan-phase'), result);
assert.ok(result.includes('/gsd-execute-phase'), result);
});
});
test('does not modify unrelated content', () => {
const input = 'This is a plain text description with no paths.';
const result = convertClaudeToAntigravityContent(input, false);
assert.strictEqual(result, input);
});
});
// ─── convertClaudeCommandToAntigravitySkill ─────────────────────────────────────
describe('convertClaudeCommandToAntigravitySkill', () => {
const claudeCommand = `---
name: gsd:new-project
description: Initialize a new GSD project with requirements and roadmap
argument-hint: "[project-name]"
allowed-tools:
- Read
- Write
- Bash
- Agent
---
Initialize new project at ~/.claude/get-shit-done/workflows/new-project.md
`;
test('produces name and description only in frontmatter', () => {
const result = convertClaudeCommandToAntigravitySkill(claudeCommand, 'gsd-new-project', false);
assert.ok(result.startsWith('---\n'), result);
assert.ok(result.includes('name: gsd-new-project'), result);
assert.ok(result.includes('description: Initialize a new GSD project'), result);
// No allowed-tools in output
assert.ok(!result.includes('allowed-tools'), result);
// No argument-hint in output
assert.ok(!result.includes('argument-hint'), result);
});
test('applies path replacement in body', () => {
const result = convertClaudeCommandToAntigravitySkill(claudeCommand, 'gsd-new-project', false);
assert.ok(result.includes('.agent/get-shit-done/'), result);
assert.ok(!result.includes('~/.claude/'), result);
});
test('uses provided skillName for name field', () => {
const result = convertClaudeCommandToAntigravitySkill(claudeCommand, 'gsd-custom-name', false);
assert.ok(result.includes('name: gsd-custom-name'), result);
});
test('converts gsd: command references in body', () => {
const content = `---
name: test
description: test skill
---
Run /gsd:new-project to get started.
`;
const result = convertClaudeCommandToAntigravitySkill(content, 'gsd-test', false);
assert.ok(result.includes('/gsd-new-project'), result);
assert.ok(!result.includes('gsd:'), result);
});
test('returns unchanged content when no frontmatter', () => {
const noFm = 'Just some text without frontmatter.';
const result = convertClaudeCommandToAntigravitySkill(noFm, 'gsd-test', false);
// Path replacements still apply, but no frontmatter transformation
assert.ok(!result.startsWith('---'), result);
});
});
// ─── convertClaudeAgentToAntigravityAgent ──────────────────────────────────────
describe('convertClaudeAgentToAntigravityAgent', () => {
const claudeAgent = `---
name: gsd-executor
description: Executes GSD plans with atomic commits
tools: Read, Write, Edit, Bash, Glob, Grep, Task
color: blue
---
Execute plans from ~/.claude/get-shit-done/workflows/execute-phase.md
`;
test('preserves name and description', () => {
const result = convertClaudeAgentToAntigravityAgent(claudeAgent, false);
assert.ok(result.includes('name: gsd-executor'), result);
assert.ok(result.includes('description: Executes GSD plans'), result);
});
test('maps Claude tools to Gemini tool names', () => {
const result = convertClaudeAgentToAntigravityAgent(claudeAgent, false);
// Read → read_file, Bash → run_shell_command
assert.ok(result.includes('read_file'), result);
assert.ok(result.includes('run_shell_command'), result);
// Original Claude names should not appear in tools line
const fmEnd = result.indexOf('---', 3);
const frontmatter = result.slice(0, fmEnd);
assert.ok(!frontmatter.includes('tools: Read,'), frontmatter);
});
test('preserves color field', () => {
const result = convertClaudeAgentToAntigravityAgent(claudeAgent, false);
assert.ok(result.includes('color: blue'), result);
});
test('applies path replacement in body', () => {
const result = convertClaudeAgentToAntigravityAgent(claudeAgent, false);
assert.ok(result.includes('.agent/get-shit-done/'), result);
assert.ok(!result.includes('~/.claude/'), result);
});
test('uses global path for global installs', () => {
const result = convertClaudeAgentToAntigravityAgent(claudeAgent, true);
assert.ok(result.includes('~/.gemini/antigravity/get-shit-done/'), result);
});
test('excludes Task tool (filtered by convertGeminiToolName)', () => {
const result = convertClaudeAgentToAntigravityAgent(claudeAgent, false);
// Task is excluded by convertGeminiToolName (returns null for Task)
const fmEnd = result.indexOf('---', 3);
const frontmatter = result.slice(0, fmEnd);
assert.ok(!frontmatter.includes('Task'), frontmatter);
});
});
// ─── copyCommandsAsAntigravitySkills ───────────────────────────────────────────
describe('copyCommandsAsAntigravitySkills', () => {
let tmpDir;
let srcDir;
let skillsDir;
beforeEach(() => {
tmpDir = createTempDir('gsd-ag-test-');
srcDir = path.join(tmpDir, 'commands', 'gsd');
skillsDir = path.join(tmpDir, 'skills');
fs.mkdirSync(srcDir, { recursive: true });
// Create a sample command file
fs.writeFileSync(path.join(srcDir, 'new-project.md'), `---
name: gsd:new-project
description: Initialize a new project
allowed-tools:
- Read
- Write
---
Run /gsd:new-project to start.
`);
// Create a subdirectory command
const subDir = path.join(srcDir, 'subdir');
fs.mkdirSync(subDir, { recursive: true });
fs.writeFileSync(path.join(subDir, 'sub-command.md'), `---
name: gsd:sub-command
description: A sub-command
allowed-tools:
- Read
---
Body text.
`);
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
test('creates skills directory', () => {
copyCommandsAsAntigravitySkills(srcDir, skillsDir, 'gsd', false);
assert.ok(fs.existsSync(skillsDir));
});
test('creates one skill directory per command with SKILL.md', () => {
copyCommandsAsAntigravitySkills(srcDir, skillsDir, 'gsd', false);
const skillDir = path.join(skillsDir, 'gsd-new-project');
assert.ok(fs.existsSync(skillDir), 'skill dir should exist');
assert.ok(fs.existsSync(path.join(skillDir, 'SKILL.md')), 'SKILL.md should exist');
});
test('handles subdirectory commands with prefixed names', () => {
copyCommandsAsAntigravitySkills(srcDir, skillsDir, 'gsd', false);
const subSkillDir = path.join(skillsDir, 'gsd-subdir-sub-command');
assert.ok(fs.existsSync(subSkillDir), 'subdirectory skill dir should exist');
});
test('SKILL.md has minimal frontmatter (name + description only)', () => {
copyCommandsAsAntigravitySkills(srcDir, skillsDir, 'gsd', false);
const content = fs.readFileSync(path.join(skillsDir, 'gsd-new-project', 'SKILL.md'), 'utf8');
assert.ok(content.includes('name: gsd-new-project'), content);
assert.ok(content.includes('description: Initialize a new project'), content);
assert.ok(!content.includes('allowed-tools'), content);
});
test('SKILL.md body has paths converted for local install', () => {
copyCommandsAsAntigravitySkills(srcDir, skillsDir, 'gsd', false);
const content = fs.readFileSync(path.join(skillsDir, 'gsd-new-project', 'SKILL.md'), 'utf8');
// gsd: → gsd- conversion
assert.ok(!content.includes('gsd:'), content);
});
test('removes old gsd-* skill dirs before reinstalling', () => {
// Create a stale skill dir
const staleDir = path.join(skillsDir, 'gsd-old-skill');
fs.mkdirSync(staleDir, { recursive: true });
fs.writeFileSync(path.join(staleDir, 'SKILL.md'), '---\nname: old\n---\n');
copyCommandsAsAntigravitySkills(srcDir, skillsDir, 'gsd', false);
assert.ok(!fs.existsSync(staleDir), 'stale skill dir should be removed');
});
test('does not remove non-gsd skill dirs', () => {
// Create a non-GSD skill dir
const otherDir = path.join(skillsDir, 'my-custom-skill');
fs.mkdirSync(otherDir, { recursive: true });
copyCommandsAsAntigravitySkills(srcDir, skillsDir, 'gsd', false);
assert.ok(fs.existsSync(otherDir), 'non-GSD skill dir should be preserved');
});
});
// ─── writeManifest (Antigravity) ───────────────────────────────────────────────
describe('writeManifest (Antigravity)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempDir('gsd-manifest-ag-');
// Create minimal structure
const skillsDir = path.join(tmpDir, 'skills', 'gsd-help');
fs.mkdirSync(skillsDir, { recursive: true });
fs.writeFileSync(path.join(skillsDir, 'SKILL.md'), '---\nname: gsd-help\ndescription: Help\n---\n');
const gsdDir = path.join(tmpDir, 'get-shit-done');
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(path.join(gsdDir, 'VERSION'), '1.0.0');
const agentsDir = path.join(tmpDir, 'agents');
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(path.join(agentsDir, 'gsd-executor.md'), '---\nname: gsd-executor\n---\n');
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
test('writes manifest JSON file', () => {
writeManifest(tmpDir, 'antigravity');
const manifestPath = path.join(tmpDir, 'gsd-file-manifest.json');
assert.ok(fs.existsSync(manifestPath), 'manifest file should exist');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
assert.ok(manifest.version, 'should have version');
assert.ok(manifest.files, 'should have files');
});
test('manifest includes skills in skills/ directory', () => {
writeManifest(tmpDir, 'antigravity');
const manifest = JSON.parse(fs.readFileSync(path.join(tmpDir, 'gsd-file-manifest.json'), 'utf8'));
const skillFiles = Object.keys(manifest.files).filter(f => f.startsWith('skills/'));
assert.ok(skillFiles.length > 0, 'should have skill files in manifest');
});
test('manifest includes agent files', () => {
writeManifest(tmpDir, 'antigravity');
const manifest = JSON.parse(fs.readFileSync(path.join(tmpDir, 'gsd-file-manifest.json'), 'utf8'));
const agentFiles = Object.keys(manifest.files).filter(f => f.startsWith('agents/'));
assert.ok(agentFiles.length > 0, 'should have agent files in manifest');
});
});