Fold augment's runtime-literal conversion branches onto descriptor-driven
hostBehaviors and delete dead code:
- Site A (_applyRuntimeRewrites case 'augment'): the 4 ~/.augment dot-dir
regexes now derive from getDirName('augment') via escapeRegExp (byte-
identical; getDirName('augment')==='.augment') — no runtime literal.
- Site B (applyRuntimeContentRewritesForCommandsInPlace): the
`if (runtime==='augment')` markdown-converter branch now reads
runtime.hostBehaviors.commandBodyConverter and dispatches through a local
COMMAND_BODY_CONVERTERS map (degrade-closed on unknown/absent name).
- Deleted dead `claudeToAugmentTools` map (zero refs; orphaned by ADR-1508
single-sourcing) and the unreachable `else if (isAugment)` agent-conversion
branch (augment ∈ _DESCRIPTOR_AGENTS_RUNTIMES → gated out upstream).
- Incidental orphan cleanup (no-defer): removed the equally-unreachable
`else if (isTrae)` agent-conversion arm left behind by trae's already-merged
migration #2094 (trae ∈ _DESCRIPTOR_AGENTS_RUNTIMES, same upstream gate).
copilot/windsurf/codebuddy arms are removed by their own pending migrations.
UPGRADE 3 (transport:mcp): register the GSD companion MCP server in Augment's
settings.json under mcpServers.gsd (Augment hosts MCP in settings.json, not a
standalone file). mergeGsdMcpServerIntoSettings mutates the in-memory settings
object finishInstall already writes (gated on hostBehaviors.mcpCompanion===
'settings-json'); non-destructive + idempotent; symmetric uninstall removal.
settings.json is golden-excluded, so no golden change. UPGRADE 1 (named/
background dispatch) + UPGRADE 2 (settings-json hook bus, Claude dialect)
were already live in production — this adds tests exercising both.
Golden: byte-identical for all 16 runtimes (folds preserve regex behavior;
MCP lives in golden-excluded settings.json) — verified by a real double-install
tree diff. Tests: declarative-reference-augment (adapter/axes/fail-closed/
undocumented-sub-axes + source-grep guard scoped to conversion-logic branches)
+ augment-upgrades (dispatch negotiation, hook-bus live install, MCP add/
idempotent/preserve/uninstall). Matrix + connect-gsd-mcp-server + a stale
install-on-your-runtime hook-ownership claim corrected; changeset (Changed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
244 lines
13 KiB
JavaScript
244 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Augment Code capability UPGRADES — ADR-1239 / #2097 (EoS/augment migration).
|
|
*
|
|
* Mirrors tests/antigravity-upgrades.test.cjs's structure. Augment already
|
|
* shipped as a declarative-CLI host prior to #2097 (nested-skill artifact
|
|
* layout, settings-json hook surface, Claude hook event dialect, dispatch
|
|
* namedDispatch/background/subagentToolkit:'full'), so UPGRADE 1 (dispatch)
|
|
* and UPGRADE 2 (hook-bus) below are REGRESSION locks on that pre-existing
|
|
* wiring, not new capabilities. UPGRADE 3 (MCP companion) IS new: #2097 adds
|
|
* `mergeGsdMcpServerIntoSettings`, registering the GSD companion MCP server
|
|
* inside Augment's own settings.json `mcpServers` block (Augment hosts MCP
|
|
* there, unlike Antigravity's standalone mcp_config.json) — non-destructively
|
|
* appending GSD's own entry while preserving any other user-configured
|
|
* `mcpServers` entries. Uninstall removes only the GSD-owned `gsd` entry.
|
|
*/
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const os = require('node:os');
|
|
const { spawnSync } = require('node:child_process');
|
|
|
|
const { runMinimalInstall, installerEnv, INSTALL_SCRIPT } = require('./helpers/install-shared.cjs');
|
|
const { cleanup } = require('./helpers.cjs');
|
|
const {
|
|
mergeGsdMcpServerIntoSettings,
|
|
} = require('../bin/install.js');
|
|
const { negotiateHostCapabilities } = require('../gsd-core/bin/lib/host-integration.cjs');
|
|
const { PACKAGE_NAME } = require('../gsd-core/bin/lib/package-identity.cjs');
|
|
|
|
const AUGMENT_CAP = JSON.parse(
|
|
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'augment', 'capability.json'), 'utf8'),
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// UPGRADE 1: dispatch — descriptor + negotiation regression lock
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('capabilities/augment/capability.json declares dispatch.namedDispatch/background/subagentToolkit for full subagent support (UPGRADE 1)', () => {
|
|
const dispatch = AUGMENT_CAP.runtime.hostIntegration.dispatch;
|
|
assert.equal(dispatch.namedDispatch, true);
|
|
assert.equal(dispatch.background, true);
|
|
assert.equal(dispatch.subagentToolkit, 'full');
|
|
});
|
|
|
|
test('negotiateHostCapabilities does NOT flatten dispatch for augment — namedDispatch/background/subagentToolkit survive negotiation (UPGRADE 1)', () => {
|
|
const { effective } = negotiateHostCapabilities(AUGMENT_CAP.runtime.hostIntegration);
|
|
assert.equal(effective.dispatch.namedDispatch, true,
|
|
'documented namedDispatch:true must survive negotiation unflattened');
|
|
assert.equal(effective.dispatch.background, true,
|
|
'documented background:true must survive negotiation unflattened (not capped to false)');
|
|
assert.equal(effective.dispatch.subagentToolkit, 'full',
|
|
'documented subagentToolkit:full must survive negotiation unflattened');
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// UPGRADE 2: hook-bus — live install proves the settings.json hook surface
|
|
// (SessionStart/PostToolUse/PreToolUse) is actually wired for augment.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
for (const scope of ['global', 'local']) {
|
|
test(`augment --${scope}: settings.json contains GSD's managed hook entries (UPGRADE 2)`, (t) => {
|
|
const { configDir, root } = runMinimalInstall({ runtime: 'augment', scope });
|
|
t.after(() => cleanup(root));
|
|
|
|
const settingsPath = path.join(configDir, 'settings.json');
|
|
assert.ok(fs.existsSync(settingsPath), `${settingsPath} must exist`);
|
|
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
assert.ok(settings.hooks && typeof settings.hooks === 'object', 'settings.hooks must be an object');
|
|
|
|
// Augment's hookEvents dialect is 'claude' (capabilities/augment/capability.json),
|
|
// so it gets the Claude-dialect event names (PostToolUse/PreToolUse), not
|
|
// antigravity's gemini-dialect (AfterTool/BeforeTool).
|
|
assert.ok(Array.isArray(settings.hooks.SessionStart) && settings.hooks.SessionStart.length > 0,
|
|
'settings.hooks.SessionStart must be registered');
|
|
assert.ok(Array.isArray(settings.hooks.PostToolUse) && settings.hooks.PostToolUse.length > 0,
|
|
'settings.hooks.PostToolUse must be registered');
|
|
assert.ok(Array.isArray(settings.hooks.PreToolUse) && settings.hooks.PreToolUse.length > 0,
|
|
'settings.hooks.PreToolUse must be registered');
|
|
|
|
const hasGsdCommand = (entries) => entries.some(
|
|
(entry) => entry && Array.isArray(entry.hooks) &&
|
|
entry.hooks.some((h) => h && typeof h.command === 'string' && h.command.includes('gsd-')),
|
|
);
|
|
assert.ok(hasGsdCommand(settings.hooks.SessionStart), 'SessionStart must reference a gsd- managed hook');
|
|
assert.ok(hasGsdCommand(settings.hooks.PostToolUse), 'PostToolUse must reference a gsd- managed hook');
|
|
assert.ok(hasGsdCommand(settings.hooks.PreToolUse), 'PreToolUse must reference a gsd- managed hook');
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// UPGRADE 3: MCP companion config — direct-unit idempotency + preservation
|
|
// (mirrors antigravity's configureAntigravityMcpConfig unit tests exactly,
|
|
// scoped to the in-memory settings object mergeGsdMcpServerIntoSettings mutates).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('mergeGsdMcpServerIntoSettings adds mcpServers.gsd with command "npx" and args including gsd-mcp-server + PACKAGE_NAME', () => {
|
|
const settings = {};
|
|
mergeGsdMcpServerIntoSettings(settings);
|
|
assert.ok(settings.mcpServers && settings.mcpServers.gsd, 'mcpServers.gsd must be present');
|
|
assert.equal(settings.mcpServers.gsd.command, 'npx');
|
|
assert.deepEqual(settings.mcpServers.gsd.args, ['-y', '-p', PACKAGE_NAME, 'gsd-mcp-server']);
|
|
});
|
|
|
|
test('mergeGsdMcpServerIntoSettings is idempotent — a second call does not clobber an existing gsd entry', () => {
|
|
const settings = {};
|
|
mergeGsdMcpServerIntoSettings(settings);
|
|
// Simulate a user hand-edit of the gsd entry after install.
|
|
settings.mcpServers.gsd.args.push('--custom-flag');
|
|
|
|
mergeGsdMcpServerIntoSettings(settings);
|
|
|
|
assert.ok(settings.mcpServers.gsd.args.includes('--custom-flag'),
|
|
"a user-owned gsd override is never clobbered (Hyrum's Law)");
|
|
assert.equal(Object.keys(settings.mcpServers).length, 1, 'exactly one mcpServers entry (gsd)');
|
|
});
|
|
|
|
test('mergeGsdMcpServerIntoSettings preserves a pre-existing unrelated mcpServers entry', () => {
|
|
const settings = {
|
|
mcpServers: {
|
|
'my-own-server': { command: 'my-tool', args: ['--flag'] },
|
|
},
|
|
};
|
|
mergeGsdMcpServerIntoSettings(settings);
|
|
|
|
assert.deepEqual(settings.mcpServers['my-own-server'], { command: 'my-tool', args: ['--flag'] });
|
|
assert.ok(settings.mcpServers.gsd);
|
|
assert.equal(Object.keys(settings.mcpServers).length, 2, 'both entries present');
|
|
});
|
|
|
|
test('mergeGsdMcpServerIntoSettings recovers a corrupted (non-object) mcpServers field', () => {
|
|
const settingsArrayCase = { mcpServers: ['not', 'an', 'object'] };
|
|
mergeGsdMcpServerIntoSettings(settingsArrayCase);
|
|
assert.ok(settingsArrayCase.mcpServers.gsd, 'array mcpServers must be recovered to an object with gsd set');
|
|
|
|
const settingsStringCase = { mcpServers: 'corrupted-string' };
|
|
mergeGsdMcpServerIntoSettings(settingsStringCase);
|
|
assert.ok(settingsStringCase.mcpServers.gsd, 'non-object mcpServers must be recovered to an object with gsd set');
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// UPGRADE 3: MCP companion config — live install (both scopes) proves the
|
|
// end-to-end wiring through finishInstall, not just the unit function.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
for (const scope of ['global', 'local']) {
|
|
test(`augment --${scope}: settings.json registers the gsd MCP companion (UPGRADE 3)`, (t) => {
|
|
const { configDir, root } = runMinimalInstall({ runtime: 'augment', scope });
|
|
t.after(() => cleanup(root));
|
|
|
|
const settingsPath = path.join(configDir, 'settings.json');
|
|
assert.ok(fs.existsSync(settingsPath), `${settingsPath} must exist`);
|
|
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
assert.ok(settings.mcpServers && settings.mcpServers.gsd, 'mcpServers.gsd must be present');
|
|
assert.equal(settings.mcpServers.gsd.command, 'npx');
|
|
assert.deepEqual(settings.mcpServers.gsd.args, ['-y', '-p', PACKAGE_NAME, 'gsd-mcp-server']);
|
|
});
|
|
}
|
|
|
|
test('augment --global: reinstalling does not duplicate or clobber the gsd MCP companion entry (live-install idempotency)', (t) => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-augment-mcp-idem-'));
|
|
t.after(() => cleanup(root));
|
|
|
|
const args = [INSTALL_SCRIPT, '--augment', '--global', '--config-dir', root];
|
|
const env = installerEnv({ HOME: root, USERPROFILE: root });
|
|
|
|
const first = spawnSync(process.execPath, args, { encoding: 'utf8', env });
|
|
assert.strictEqual(first.status, 0, `first install failed: ${first.stderr}`);
|
|
|
|
const settingsPath = path.join(root, 'settings.json');
|
|
const afterFirst = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
assert.ok(afterFirst.mcpServers.gsd);
|
|
// Simulate a user hand-edit of the gsd entry between installs.
|
|
afterFirst.mcpServers.gsd.args.push('--custom-flag');
|
|
fs.writeFileSync(settingsPath, JSON.stringify(afterFirst, null, 2) + '\n');
|
|
|
|
const second = spawnSync(process.execPath, args, { encoding: 'utf8', env });
|
|
assert.strictEqual(second.status, 0, `second install failed: ${second.stderr}`);
|
|
|
|
const afterSecond = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
assert.equal(Object.keys(afterSecond.mcpServers).length, 1, 'reinstall must not duplicate the gsd entry');
|
|
assert.ok(afterSecond.mcpServers.gsd.args.includes('--custom-flag'),
|
|
"a user-owned gsd override is never clobbered across reinstall (Hyrum's Law)");
|
|
});
|
|
|
|
test('augment --global: installing preserves a pre-existing unrelated mcpServers entry (live-install preservation)', (t) => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-augment-mcp-preserve-'));
|
|
fs.mkdirSync(root, { recursive: true });
|
|
t.after(() => cleanup(root));
|
|
|
|
// Pre-seed settings.json with a user's own MCP server BEFORE install runs.
|
|
fs.writeFileSync(path.join(root, 'settings.json'), JSON.stringify({
|
|
mcpServers: {
|
|
other: { command: 'my-tool', args: ['--flag'] },
|
|
},
|
|
}, null, 2));
|
|
|
|
const args = [INSTALL_SCRIPT, '--augment', '--global', '--config-dir', root];
|
|
const result = spawnSync(process.execPath, args, {
|
|
encoding: 'utf8',
|
|
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
|
});
|
|
assert.strictEqual(result.status, 0, `install failed: ${result.stderr}`);
|
|
|
|
const settings = JSON.parse(fs.readFileSync(path.join(root, 'settings.json'), 'utf8'));
|
|
assert.deepEqual(settings.mcpServers.other, { command: 'my-tool', args: ['--flag'] },
|
|
"the user's pre-existing mcpServers entry must be preserved");
|
|
assert.ok(settings.mcpServers.gsd, 'the gsd companion entry must also be present');
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Uninstall — symmetric cleanup for the MCP companion entry.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('augment --global uninstall removes only the GSD-owned mcpServers.gsd entry, preserving user data', (t) => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-augment-uninstall-'));
|
|
t.after(() => cleanup(root));
|
|
|
|
const env = installerEnv({ HOME: root, USERPROFILE: root });
|
|
const installArgs = [INSTALL_SCRIPT, '--augment', '--global', '--config-dir', root];
|
|
const installResult = spawnSync(process.execPath, installArgs, { encoding: 'utf8', env });
|
|
assert.strictEqual(installResult.status, 0, `install failed: ${installResult.stderr}`);
|
|
|
|
// Seed user-owned data alongside GSD's contributions, post-install.
|
|
const settingsPath = path.join(root, 'settings.json');
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
settings.mcpServers['my-own-server'] = { command: 'my-tool', args: [] };
|
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
|
|
const uninstallArgs = [INSTALL_SCRIPT, '--augment', '--global', '--config-dir', root, '--uninstall'];
|
|
const uninstallResult = spawnSync(process.execPath, uninstallArgs, { encoding: 'utf8', env });
|
|
assert.strictEqual(uninstallResult.status, 0, `uninstall failed: ${uninstallResult.stderr}`);
|
|
|
|
const settingsAfter = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
assert.equal(settingsAfter.mcpServers && settingsAfter.mcpServers.gsd, undefined, 'gsd MCP entry removed');
|
|
assert.deepEqual(settingsAfter.mcpServers['my-own-server'], { command: 'my-tool', args: [] },
|
|
'user MCP server preserved');
|
|
});
|