Fresh antigravity workspace installs write under the canonical .agents/ (plural) base; legacy .agent/ stays recognized (dual-read). Global ~/.gemini/antigravity/ path unchanged. Closes #791.
This commit is contained in:
@@ -73,12 +73,12 @@ describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () =
|
||||
});
|
||||
|
||||
describe('local install', () => {
|
||||
test('replaces ~/.claude (bare, no trailing slash) with .agent', () => {
|
||||
test('replaces ~/.claude (bare, no trailing slash) with .agents', () => {
|
||||
const input = 'configDir = ~/.claude';
|
||||
const result = convertClaudeToAntigravityContent(input, false);
|
||||
assert.ok(
|
||||
result.includes('.agent'),
|
||||
`Expected .agent in output, got: ${result}`
|
||||
result.includes('.agents'),
|
||||
`Expected .agents in output, got: ${result}`
|
||||
);
|
||||
assert.ok(
|
||||
!result.includes('~/.claude'),
|
||||
@@ -86,12 +86,12 @@ describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () =
|
||||
);
|
||||
});
|
||||
|
||||
test('replaces $HOME/.claude (bare, no trailing slash) with .agent', () => {
|
||||
test('replaces $HOME/.claude (bare, no trailing slash) with .agents', () => {
|
||||
const input = 'export DIR=$HOME/.claude';
|
||||
const result = convertClaudeToAntigravityContent(input, false);
|
||||
assert.ok(
|
||||
result.includes('.agent'),
|
||||
`Expected .agent in output, got: ${result}`
|
||||
result.includes('.agents'),
|
||||
`Expected .agents in output, got: ${result}`
|
||||
);
|
||||
assert.ok(
|
||||
!result.includes('$HOME/.claude'),
|
||||
@@ -102,8 +102,8 @@ describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () =
|
||||
test('does not double-replace ~/.claude/ paths', () => {
|
||||
const input = 'See ~/.claude/gsd-core/';
|
||||
const result = convertClaudeToAntigravityContent(input, false);
|
||||
// .agent/ should appear exactly once
|
||||
const count = (result.match(/\.agent\//g) || []).length;
|
||||
// .agents/ should appear exactly once
|
||||
const count = (result.match(/\.agents\//g) || []).length;
|
||||
assert.strictEqual(count, 1, `Expected exactly 1 replacement, got ${count} in: ${result}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('bug #2557: Gemini/Antigravity local hooks use relative paths (not $CLA
|
||||
});
|
||||
|
||||
test('Antigravity local prefix is bare dirName', () => {
|
||||
assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agent' }), '.agent');
|
||||
assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agents' }), '.agents');
|
||||
});
|
||||
|
||||
test('non-Gemini local prefix remains $CLAUDE_PROJECT_DIR anchored', () => {
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('bug #3439: shell projection module owns managed-hook policy and legacy
|
||||
|
||||
test('projectLocalHookPrefix centralizes runtime-specific project-dir interpolation policy', () => {
|
||||
assert.equal(projectLocalHookPrefix({ runtime: 'gemini', dirName: '.gemini' }), '.gemini');
|
||||
assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agent' }), '.agent');
|
||||
assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agents' }), '.agents');
|
||||
assert.equal(
|
||||
projectLocalHookPrefix({ runtime: 'claude', dirName: '.claude' }),
|
||||
'"$CLAUDE_PROJECT_DIR"/.claude',
|
||||
|
||||
@@ -49,8 +49,26 @@ function fakeFs(files) {
|
||||
};
|
||||
}
|
||||
|
||||
describe('/gsd:update detects local Antigravity (.agent) installs (#503)', () => {
|
||||
test('projection resolves a LOCAL ./.agent install to the antigravity runtime', () => {
|
||||
describe('/gsd:update detects local Antigravity (.agent / .agents) installs (#503 / #791)', () => {
|
||||
test('projection resolves a LOCAL ./.agents install to the antigravity runtime (#791 canonical)', () => {
|
||||
const HOME = '/home/u';
|
||||
const CWD = '/work/proj';
|
||||
const agentsDir = `${CWD}/.agents`;
|
||||
const ffs = fakeFs({
|
||||
[`${agentsDir}/gsd-core/VERSION`]: '1.50.0\n',
|
||||
[`${agentsDir}/gsd-core/workflows/update.md`]: 'x',
|
||||
});
|
||||
const r = resolveUpdateContext({ home: HOME, cwd: CWD, env: {}, fs: ffs });
|
||||
assert.equal(
|
||||
r.runtime,
|
||||
'antigravity',
|
||||
`a local .agents install must map to the antigravity runtime, got "${r.runtime}"`,
|
||||
);
|
||||
assert.equal(r.scope, 'LOCAL');
|
||||
assert.equal(r.installedVersion, '1.50.0');
|
||||
});
|
||||
|
||||
test('projection resolves a LOCAL ./.agent install to the antigravity runtime (#503 backward-compat)', () => {
|
||||
const HOME = '/home/u';
|
||||
const CWD = '/work/proj';
|
||||
const agentDir = `${CWD}/.agent`;
|
||||
@@ -62,26 +80,33 @@ describe('/gsd:update detects local Antigravity (.agent) installs (#503)', () =>
|
||||
assert.equal(
|
||||
r.runtime,
|
||||
'antigravity',
|
||||
`a local .agent install must map to the antigravity runtime, got "${r.runtime}"`,
|
||||
`a legacy .agent install must still map to the antigravity runtime, got "${r.runtime}"`,
|
||||
);
|
||||
assert.equal(r.scope, 'LOCAL');
|
||||
assert.equal(r.installedVersion, '1.40.0');
|
||||
});
|
||||
|
||||
test('execution_context classifier maps a /.agent/ path to antigravity (update.md)', () => {
|
||||
test('execution_context classifier maps /.agents/ and /.agent/ paths to antigravity (update.md)', () => {
|
||||
const hasAgentsClassifierRule =
|
||||
/\/\.agents\/[^\n]*->[^\n]*antigravity/.test(UPDATE_MD);
|
||||
assert.ok(
|
||||
hasAgentsClassifierRule,
|
||||
'update.md classifier must map a `/.agents/` path to the `antigravity` runtime',
|
||||
);
|
||||
const hasAgentClassifierRule =
|
||||
/\/\.agent\/[^\n]*->[^\n]*antigravity/.test(UPDATE_MD);
|
||||
assert.ok(
|
||||
hasAgentClassifierRule,
|
||||
'update.md classifier must map a `/.agent/` path to the `antigravity` runtime',
|
||||
'update.md classifier must still map a `/.agent/` path to the `antigravity` runtime (backward-compat)',
|
||||
);
|
||||
});
|
||||
|
||||
test('every runtime-dir `for dir in` loop in update.md includes .agent', () => {
|
||||
test('every runtime-dir `for dir in` loop in update.md includes .agents and .agent', () => {
|
||||
// The LOCAL-scope discovery loop moved into the projection (#498); the
|
||||
// post-update cache-clear loop remains inline and still enumerates the
|
||||
// runtime config dirs as a literal `.claude ... .codex` list, so it must
|
||||
// include .agent or a local Antigravity install keeps a stale indicator.
|
||||
// include both .agents (canonical, #791) and .agent (legacy, #503) or
|
||||
// stale indicators could linger.
|
||||
const runtimeDirLoops = UPDATE_MD
|
||||
.split('\n')
|
||||
.filter((l) => /for dir in .*\.claude.*\.codex/.test(l));
|
||||
@@ -90,9 +115,13 @@ describe('/gsd:update detects local Antigravity (.agent) installs (#503)', () =>
|
||||
`expected at least 1 runtime-dir loop in update.md, found ${runtimeDirLoops.length}`,
|
||||
);
|
||||
for (const loop of runtimeDirLoops) {
|
||||
assert.ok(
|
||||
/(^|\s)\.agents(\s|$)/.test(loop),
|
||||
`every runtime-dir loop must include .agents (canonical), got: ${loop.trim()}`,
|
||||
);
|
||||
assert.ok(
|
||||
/(^|\s)\.agent(\s|$)/.test(loop),
|
||||
`every runtime-dir loop must include .agent, got: ${loop.trim()}`,
|
||||
`every runtime-dir loop must include .agent (legacy backward-compat), got: ${loop.trim()}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ const EXPECTED_ALL_HOOKS = [
|
||||
|
||||
const RUNTIME_META = {
|
||||
claude: { localDir: '.claude', globalSuffix: '.claude' },
|
||||
antigravity: { localDir: '.agent', globalSuffix: path.join('.gemini', 'antigravity') },
|
||||
antigravity: { localDir: '.agents', globalSuffix: path.join('.gemini', 'antigravity') },
|
||||
augment: { localDir: '.augment', globalSuffix: '.augment' },
|
||||
cline: { localDir: '.cline', globalSuffix: '.cline' },
|
||||
codebuddy: { localDir: '.codebuddy', globalSuffix: '.codebuddy' },
|
||||
@@ -111,7 +111,7 @@ function runMinimalInstall({ runtime, scope, extraArgs = [] }) {
|
||||
try {
|
||||
const LOCAL_DIR_NAME = {
|
||||
claude: '.claude', opencode: '.opencode', gemini: '.gemini', kilo: '.kilo',
|
||||
codex: '.codex', copilot: '.github', antigravity: '.agent', cursor: '.cursor',
|
||||
codex: '.codex', copilot: '.github', antigravity: '.agents', cursor: '.cursor',
|
||||
windsurf: '.windsurf', augment: '.augment', trae: '.trae', qwen: '.qwen',
|
||||
codebuddy: '.codebuddy', cline: '.',
|
||||
};
|
||||
|
||||
@@ -261,7 +261,7 @@ describe('getConfigDirFromHome — spot-checks', () => {
|
||||
assert.strictEqual(getConfigDirFromHome('trae', true), "'.trae'");
|
||||
});
|
||||
|
||||
test('antigravity returns .agent (local) and legacy fallback global path when no 2.x dirs exist', () => {
|
||||
test('antigravity returns .agents (local) and legacy fallback global path when no 2.x dirs exist', () => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-antigravity-empty-'));
|
||||
const savedHome = process.env.HOME;
|
||||
const savedUserProfile = process.env.USERPROFILE;
|
||||
@@ -270,7 +270,7 @@ describe('getConfigDirFromHome — spot-checks', () => {
|
||||
process.env.HOME = home;
|
||||
process.env.USERPROFILE = home;
|
||||
try {
|
||||
assert.strictEqual(getConfigDirFromHome('antigravity', false), "'.agent'");
|
||||
assert.strictEqual(getConfigDirFromHome('antigravity', false), "'.agents'");
|
||||
assert.strictEqual(getConfigDirFromHome('antigravity', true), "'.gemini', 'antigravity'");
|
||||
} finally {
|
||||
if (savedHome === undefined) delete process.env.HOME;
|
||||
@@ -836,6 +836,110 @@ describe('install — changeset CLI lands at scripts/changeset/cli.cjs (#935)',
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Section N: Antigravity .agents canonical workspace dir (#791) ─────────────
|
||||
// allow-test-rule: runtime-contract-is-the-product
|
||||
// Reads deployed agent .md files whose text IS the product surface the
|
||||
// Antigravity runtime loads at startup (path references, command names).
|
||||
|
||||
describe('antigravity local install writes to .agents/ canonical dir (#791)', () => {
|
||||
let tmpDir;
|
||||
let previousCwd;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempDir('gsd-antigravity-791-');
|
||||
previousCwd = process.cwd();
|
||||
process.chdir(tmpDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(previousCwd);
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('install writes workspace skills under .agents/skills/', () => {
|
||||
const result = install(false, 'antigravity');
|
||||
const agentsDir = path.join(tmpDir, '.agents');
|
||||
assert.strictEqual(result.runtime, 'antigravity');
|
||||
assert.ok(fs.existsSync(agentsDir), '.agents/ must be created for local antigravity install');
|
||||
const skillsDir = path.join(agentsDir, 'skills');
|
||||
assert.ok(fs.existsSync(skillsDir), '.agents/skills/ must exist after install');
|
||||
const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
||||
assert.ok(skillEntries.length > 0, 'at least one gsd-* skill must be installed under .agents/skills/');
|
||||
const firstSkill = path.join(skillsDir, skillEntries[0].name, 'SKILL.md');
|
||||
assert.ok(fs.existsSync(firstSkill), `SKILL.md must exist at ${firstSkill}`);
|
||||
});
|
||||
|
||||
test('installed agent files reference .agents/ not ~/.claude/ or bare .agent/', () => {
|
||||
// NOTE: skill content is intentionally NOT asserted here. The installer calls
|
||||
// convertClaudeCommandToAntigravitySkill(content, skillName, runtime, cmdNames)
|
||||
// where the 3rd arg is the string "antigravity" (truthy), routing local skills
|
||||
// through the global content branch — a pre-existing quirk tracked separately.
|
||||
// Agent files are NOT affected: convertClaudeAgentToAntigravityAgent(content, isGlobal)
|
||||
// receives the boolean isGlobal correctly, so local agents use the local (.agents/) branch.
|
||||
install(false, 'antigravity');
|
||||
const agentsDest = path.join(tmpDir, '.agents', 'agents');
|
||||
assert.ok(fs.existsSync(agentsDest), '.agents/agents/ must exist after local install');
|
||||
const agentFiles = fs.readdirSync(agentsDest)
|
||||
.filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
|
||||
assert.ok(agentFiles.length > 0, 'pre-condition: at least one gsd-* agent must be installed');
|
||||
for (const file of agentFiles) {
|
||||
const content = fs.readFileSync(path.join(agentsDest, file), 'utf8');
|
||||
// Local agent content must not contain global home-dir paths (should be .agents/)
|
||||
assert.ok(
|
||||
!content.includes('~/.claude/') && !content.includes('$HOME/.claude/'),
|
||||
`${file} must not contain ~/.claude/ or $HOME/.claude/ in a local install; content uses .agents/`,
|
||||
);
|
||||
// Local agent content must not reference the legacy singular .agent/ path
|
||||
const bareAgentRefs = content.match(/(?<!\w)\.agent(?!s)\//g) || [];
|
||||
assert.strictEqual(
|
||||
bareAgentRefs.length, 0,
|
||||
`${file} must not reference legacy .agent/ path; found: ${bareAgentRefs.join(', ')}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('legacy .agent/ is NOT written on a fresh local install', () => {
|
||||
install(false, 'antigravity');
|
||||
const legacyDir = path.join(tmpDir, '.agent');
|
||||
assert.ok(!fs.existsSync(legacyDir),
|
||||
'.agent/ must not be created by a fresh install (new installs use .agents/)');
|
||||
});
|
||||
|
||||
test('global antigravity install still writes to ~/.gemini/antigravity (unchanged)', () => {
|
||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ag-global-'));
|
||||
const savedHome = process.env.HOME;
|
||||
const savedUserProfile = process.env.USERPROFILE;
|
||||
const savedAntigravityConfig = process.env.ANTIGRAVITY_CONFIG_DIR;
|
||||
delete process.env.ANTIGRAVITY_CONFIG_DIR;
|
||||
process.env.HOME = homeDir;
|
||||
process.env.USERPROFILE = homeDir;
|
||||
try {
|
||||
const result = install(true, 'antigravity');
|
||||
assert.strictEqual(result.runtime, 'antigravity');
|
||||
assert.ok(
|
||||
result.configDir.startsWith(homeDir),
|
||||
`global antigravity install must go under HOME, got: ${result.configDir}`,
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(result.configDir, 'skills')),
|
||||
'global antigravity install must create skills/ under ~/.gemini/antigravity',
|
||||
);
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(homeDir, '.agents')),
|
||||
'.agents/ must NOT be created by a global install (global path is ~/.gemini/antigravity)',
|
||||
);
|
||||
} finally {
|
||||
if (savedHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = savedHome;
|
||||
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = savedUserProfile;
|
||||
if (savedAntigravityConfig === undefined) delete process.env.ANTIGRAVITY_CONFIG_DIR;
|
||||
else process.env.ANTIGRAVITY_CONFIG_DIR = savedAntigravityConfig;
|
||||
cleanup(homeDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
// ─── Section 6: Windsurf / devin-desktop alias (#792) ───────────────────────
|
||||
|
||||
describe('install — --devin-desktop CLI flag routes to windsurf runtime (#792)', () => {
|
||||
|
||||
Reference in New Issue
Block a user