Merge pull request #2166 from open-gsd/feat/2097-eos-augment
feat(#2097): migrate Augment onto EoS declarative adapter + settings.json MCP companion (ADR-1239)
This commit is contained in:
6
.changeset/2097-eos-augment-imperative-adapter.md
Normal file
6
.changeset/2097-eos-augment-imperative-adapter.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
type: Changed
|
||||||
|
pr: 2166
|
||||||
|
---
|
||||||
|
|
||||||
|
**Augment Code now installs through its capability descriptor, with a native MCP companion** — installing GSD into Augment registers the GSD companion server in Augment's `settings.json` `mcpServers` and drives command/skill/agent conversion from Augment's negotiated descriptor instead of hardcoded runtime special-cases. (#2097)
|
||||||
@@ -2606,14 +2606,6 @@ function convertClaudeAgentToWindsurfAgent(content) {
|
|||||||
// Augment uses a tool set similar to Cursor/Windsurf.
|
// Augment uses a tool set similar to Cursor/Windsurf.
|
||||||
// Config lives in .augment/ (local) and ~/.augment/ (global).
|
// Config lives in .augment/ (local) and ~/.augment/ (global).
|
||||||
|
|
||||||
const claudeToAugmentTools = {
|
|
||||||
Bash: 'launch-process',
|
|
||||||
Edit: 'str-replace-editor',
|
|
||||||
AskUserQuestion: null,
|
|
||||||
SlashCommand: null,
|
|
||||||
TodoWrite: 'add_tasks',
|
|
||||||
};
|
|
||||||
|
|
||||||
// #1675 (ADR-1508): the augment converter family below was a byte-identical
|
// #1675 (ADR-1508): the augment converter family below was a byte-identical
|
||||||
// duplicate of runtime-artifact-conversion.cjs:
|
// duplicate of runtime-artifact-conversion.cjs:
|
||||||
// convertSlashCommandsToAugmentSkillMentions, convertClaudeToAugmentMarkdown,
|
// convertSlashCommandsToAugmentSkillMentions, convertClaudeToAugmentMarkdown,
|
||||||
@@ -7546,6 +7538,21 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #2097 UPGRADE 3 — Remove the MCP companion entry from settings.json for
|
||||||
|
// runtimes that host MCP there (Augment), symmetric to the mcp_config.json
|
||||||
|
// removal for Antigravity below. Only the GSD-owned mcpServers.gsd key is
|
||||||
|
// removed — any other user-configured MCP servers are preserved.
|
||||||
|
if (_hostBehaviors(runtime).mcpCompanion === 'settings-json' &&
|
||||||
|
settings.mcpServers && typeof settings.mcpServers === 'object' &&
|
||||||
|
settings.mcpServers.gsd !== undefined) {
|
||||||
|
delete settings.mcpServers.gsd;
|
||||||
|
if (Object.keys(settings.mcpServers).length === 0) {
|
||||||
|
delete settings.mcpServers;
|
||||||
|
}
|
||||||
|
settingsModified = true;
|
||||||
|
console.log(` ${green}✓${reset} Removed GSD MCP companion server from settings.json`);
|
||||||
|
}
|
||||||
|
|
||||||
if (settingsModified) {
|
if (settingsModified) {
|
||||||
writeSettings(settingsPath, settings);
|
writeSettings(settingsPath, settings);
|
||||||
removedCount++;
|
removedCount++;
|
||||||
@@ -8078,6 +8085,27 @@ function configureAntigravityMcpConfig(isGlobal = true, configDir = null) {
|
|||||||
console.log(` ${green}✓${reset} Configured Antigravity MCP companion server (gsd)`);
|
console.log(` ${green}✓${reset} Configured Antigravity MCP companion server (gsd)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* #2097 (ADR-1239 transport:mcp): register the GSD companion MCP server inside a
|
||||||
|
* runtime's settings.json (Augment hosts MCP in settings.json.mcpServers, unlike
|
||||||
|
* Antigravity's standalone mcp_config.json). Mutates the in-memory settings object
|
||||||
|
* that finishInstall already writes — non-destructive + idempotent: only sets
|
||||||
|
* mcpServers.gsd, preserving any user-defined servers (a user's own `gsd` override
|
||||||
|
* is respected — Hyrum's Law).
|
||||||
|
* @param {object} settings - the in-memory settings object finishInstall will write
|
||||||
|
*/
|
||||||
|
function mergeGsdMcpServerIntoSettings(settings) {
|
||||||
|
if (!settings.mcpServers || typeof settings.mcpServers !== 'object' || Array.isArray(settings.mcpServers)) {
|
||||||
|
settings.mcpServers = {};
|
||||||
|
}
|
||||||
|
if (settings.mcpServers.gsd === undefined) {
|
||||||
|
settings.mcpServers.gsd = {
|
||||||
|
command: 'npx',
|
||||||
|
args: ['-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify a directory exists and contains files
|
* Verify a directory exists and contains files
|
||||||
*/
|
*/
|
||||||
@@ -9643,10 +9671,6 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|||||||
content = convertClaudeAgentToCopilotAgent(content, isGlobal);
|
content = convertClaudeAgentToCopilotAgent(content, isGlobal);
|
||||||
} else if (isWindsurf) {
|
} else if (isWindsurf) {
|
||||||
content = convertClaudeAgentToWindsurfAgent(content);
|
content = convertClaudeAgentToWindsurfAgent(content);
|
||||||
} else if (isAugment) {
|
|
||||||
content = convertClaudeAgentToAugmentAgent(content);
|
|
||||||
} else if (isTrae) {
|
|
||||||
content = convertClaudeAgentToTraeAgent(content);
|
|
||||||
} else if (isCodebuddy) {
|
} else if (isCodebuddy) {
|
||||||
content = convertClaudeAgentToCodebuddyAgent(content);
|
content = convertClaudeAgentToCodebuddyAgent(content);
|
||||||
} else if (_hostBehaviors(runtime).frontmatterDialect === 'cline') {
|
} else if (_hostBehaviors(runtime).frontmatterDialect === 'cline') {
|
||||||
@@ -10918,6 +10942,12 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|||||||
mergeClaudePermissions(settings);
|
mergeClaudePermissions(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #2097 UPGRADE 3 (transport:mcp): companion MCP server for runtimes that host
|
||||||
|
// MCP in settings.json (Augment). settings.json is golden-excluded, so no golden change.
|
||||||
|
if (_hostBehaviors(runtime).mcpCompanion === 'settings-json' && settings && plan.writesSharedSettings) {
|
||||||
|
mergeGsdMcpServerIntoSettings(settings);
|
||||||
|
}
|
||||||
|
|
||||||
// Write settings when runtime supports settings.json.
|
// Write settings when runtime supports settings.json.
|
||||||
// #3002 CR: defense-in-depth — re-run validateHookFields right before
|
// #3002 CR: defense-in-depth — re-run validateHookFields right before
|
||||||
// serialization. The push-site guards above already skip null-command
|
// serialization. The push-site guards above already skip null-command
|
||||||
@@ -11930,6 +11960,8 @@ module.exports = {
|
|||||||
buildAntigravityAllowRules,
|
buildAntigravityAllowRules,
|
||||||
configureAntigravityPermissions,
|
configureAntigravityPermissions,
|
||||||
configureAntigravityMcpConfig,
|
configureAntigravityMcpConfig,
|
||||||
|
// #2097 UPGRADE 3 — Augment MCP companion (settings.json-hosted)
|
||||||
|
mergeGsdMcpServerIntoSettings,
|
||||||
claudeToCopilotTools,
|
claudeToCopilotTools,
|
||||||
convertCopilotToolName,
|
convertCopilotToolName,
|
||||||
convertClaudeToCopilotContent,
|
convertClaudeToCopilotContent,
|
||||||
|
|||||||
@@ -82,6 +82,10 @@
|
|||||||
"writesSharedSettings": true,
|
"writesSharedSettings": true,
|
||||||
"permissionWriter": null,
|
"permissionWriter": null,
|
||||||
"extendedHookEvents": [],
|
"extendedHookEvents": [],
|
||||||
|
"hostBehaviors": {
|
||||||
|
"commandBodyConverter": "convertClaudeToAugmentMarkdown",
|
||||||
|
"mcpCompanion": "settings-json"
|
||||||
|
},
|
||||||
"hostIntegration": {
|
"hostIntegration": {
|
||||||
"embeddingMode": "declarative",
|
"embeddingMode": "declarative",
|
||||||
"commandSurface": "slash-file",
|
"commandSurface": "slash-file",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# How to connect a host to the GSD companion MCP server
|
# How to connect a host to the GSD companion MCP server
|
||||||
|
|
||||||
This guide shows you how to make a MCP-capable host (Claude Code, Codex,
|
This guide shows you how to make a MCP-capable host (Claude Code, Codex,
|
||||||
OpenCode, VS Code, Antigravity CLI, Cursor, Cline, Hermes) drive GSD — run GSD
|
OpenCode, VS Code, Antigravity CLI, Cursor, Cline, Hermes, Augment Code) drive
|
||||||
|
GSD — run GSD
|
||||||
commands and read/write `.planning/` state — through the companion MCP server,
|
commands and read/write `.planning/` state — through the companion MCP server,
|
||||||
with no bespoke plugin.
|
with no bespoke plugin.
|
||||||
|
|
||||||
@@ -28,6 +29,11 @@ host.
|
|||||||
|
|
||||||
- **Claude Code / Codex / Cursor / Cline / Hermes** — under the host's
|
- **Claude Code / Codex / Cursor / Cline / Hermes** — under the host's
|
||||||
`mcpServers` object (project or user config).
|
`mcpServers` object (project or user config).
|
||||||
|
- **Augment Code** — under the `mcpServers` block of its own
|
||||||
|
`settings.json` (not a standalone MCP config file, unlike Antigravity)
|
||||||
|
— global at `~/.augment/settings.json`, project-local at
|
||||||
|
`.augment/settings.json`. GSD's installer configures this entry
|
||||||
|
automatically (`--augment` installs).
|
||||||
- **VS Code** — in the workspace MCP servers list.
|
- **VS Code** — in the workspace MCP servers list.
|
||||||
- **Antigravity** — under the `mcpServers` block of its standalone
|
- **Antigravity** — under the `mcpServers` block of its standalone
|
||||||
`mcp_config.json` profile (not embedded in `settings.json`) — global at
|
`mcp_config.json` profile (not embedded in `settings.json`) — global at
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ Qwen Code supports 15 hook events. GSD registers the following events automatica
|
|||||||
npx @opengsd/gsd-core@latest --augment --global
|
npx @opengsd/gsd-core@latest --augment --global
|
||||||
```
|
```
|
||||||
|
|
||||||
Skills land in `~/.augment/skills/` and slash command definitions land in `~/.augment/commands/`. GSD installs skills, agents, and commands (`/gsd-phase`, `/gsd-ship`, etc.). No hook or statusline ownership.
|
Skills land in `~/.augment/skills/` and slash command definitions land in `~/.augment/commands/`. GSD installs skills, agents, and commands (`/gsd-phase`, `/gsd-ship`, etc.). GSD's managed lifecycle hooks are registered into Augment's own `settings.json` `hooks` block (Claude hook event dialect, covering session-start, tool-use, and phase-boundary events) — no statusline ownership. #2097 also registers the GSD companion MCP server under `settings.json`'s `mcpServers.gsd` (see [Connect a host to the GSD MCP server](connect-gsd-mcp-server.md)).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -334,6 +334,35 @@ Documentation gaps:
|
|||||||
- dispatch.nested
|
- dispatch.nested
|
||||||
- dispatch.maxDepth
|
- dispatch.maxDepth
|
||||||
|
|
||||||
|
**EoS migration status (#2097):** Folded onto descriptor-driven dispatch.
|
||||||
|
Augment already installed through the declarative adapter (nested-skill
|
||||||
|
artifact layout, `settings-json` hook surface, Claude hook event dialect), but
|
||||||
|
carried two remaining runtime-literal branches in
|
||||||
|
`src/runtime-artifact-conversion.cts`: the 4 `~/.augment`/`$HOME/.augment`
|
||||||
|
dot-dir rewrites in `_applyRuntimeRewrites`'s `case 'augment':` block are now
|
||||||
|
built from `getDirName('augment')` (dirName-derived, byte-identical) instead
|
||||||
|
of a hardcoded `.augment` literal, and the
|
||||||
|
`applyRuntimeContentRewritesForCommandsInPlace` command-body conversion
|
||||||
|
dispatch now reads `runtime.hostBehaviors.commandBodyConverter`
|
||||||
|
(`"convertClaudeToAugmentMarkdown"`) instead of a hardcoded
|
||||||
|
`runtime === 'augment'` branch. Two dead-code sites were also removed from
|
||||||
|
`bin/install.js`: the orphaned `claudeToAugmentTools` map (superseded by the
|
||||||
|
single-sourced converters per ADR-1508 / #1675) and the unreachable
|
||||||
|
`else if (isAugment) { content = convertClaudeAgentToAugmentAgent(content); }`
|
||||||
|
inline agent-conversion branch (augment has been on the descriptor-agents path
|
||||||
|
since `_DESCRIPTOR_AGENTS_RUNTIMES` was introduced, making that `if`/`else if`
|
||||||
|
arm dead). **UPGRADE 3 — MCP companion config**
|
||||||
|
(`mergeGsdMcpServerIntoSettings`) registers the `gsd` MCP server directly
|
||||||
|
inside the same `settings.json` `mcpServers` block GSD's own hook
|
||||||
|
registration already writes (Augment hosts MCP in `settings.json`, unlike
|
||||||
|
Antigravity's standalone `mcp_config.json`) — non-destructively preserving
|
||||||
|
any other user-configured `mcpServers` entries; uninstall removes only the
|
||||||
|
GSD-owned `gsd` entry. `settings.json` is golden-excluded
|
||||||
|
(`HOOK_CONFIG_FILES`), so this upgrade produces no golden fixture change.
|
||||||
|
Source-grep guard + fail-closed negotiation coverage is in
|
||||||
|
`tests/declarative-reference-augment.test.cjs`; the dispatch/hook-bus/MCP
|
||||||
|
upgrade coverage is in `tests/augment-upgrades.test.cjs`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## qwen
|
## qwen
|
||||||
|
|||||||
@@ -360,6 +360,10 @@ const capabilities = {
|
|||||||
"writesSharedSettings": true,
|
"writesSharedSettings": true,
|
||||||
"permissionWriter": null,
|
"permissionWriter": null,
|
||||||
"extendedHookEvents": [],
|
"extendedHookEvents": [],
|
||||||
|
"hostBehaviors": {
|
||||||
|
"commandBodyConverter": "convertClaudeToAugmentMarkdown",
|
||||||
|
"mcpCompanion": "settings-json"
|
||||||
|
},
|
||||||
"hostIntegration": {
|
"hostIntegration": {
|
||||||
"embeddingMode": "declarative",
|
"embeddingMode": "declarative",
|
||||||
"commandSurface": "slash-file",
|
"commandSurface": "slash-file",
|
||||||
@@ -3953,6 +3957,10 @@ const runtimes = {
|
|||||||
"writesSharedSettings": true,
|
"writesSharedSettings": true,
|
||||||
"permissionWriter": null,
|
"permissionWriter": null,
|
||||||
"extendedHookEvents": [],
|
"extendedHookEvents": [],
|
||||||
|
"hostBehaviors": {
|
||||||
|
"commandBodyConverter": "convertClaudeToAugmentMarkdown",
|
||||||
|
"mcpCompanion": "settings-json"
|
||||||
|
},
|
||||||
"hostIntegration": {
|
"hostIntegration": {
|
||||||
"embeddingMode": "declarative",
|
"embeddingMode": "declarative",
|
||||||
"commandSurface": "slash-file",
|
"commandSurface": "slash-file",
|
||||||
|
|||||||
@@ -1107,6 +1107,11 @@ function convertClaudeToAugmentMarkdown(content) {
|
|||||||
return converted;
|
return converted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #2097 (ADR-1239): command-body converters selected by descriptor
|
||||||
|
// (runtime.hostBehaviors.commandBodyConverter) instead of a runtime-name
|
||||||
|
// branch. Degrade-closed: unknown/absent name → no conversion.
|
||||||
|
const COMMAND_BODY_CONVERTERS = { convertClaudeToAugmentMarkdown };
|
||||||
|
|
||||||
function getAugmentSkillAdapterHeader(skillName) {
|
function getAugmentSkillAdapterHeader(skillName) {
|
||||||
return `<augment_skill_adapter>
|
return `<augment_skill_adapter>
|
||||||
## A. Skill Invocation
|
## A. Skill Invocation
|
||||||
@@ -2442,19 +2447,24 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'augment':
|
case 'augment': {
|
||||||
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
||||||
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
||||||
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
||||||
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
||||||
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
||||||
content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
|
content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
|
||||||
content = content.replace(/~\/\.augment\//g, pathPrefix);
|
// #2097: dot-dir self-references (~/.augment/…) → resolved prefix,
|
||||||
content = content.replace(/\$HOME\/\.augment\//g, pathPrefix);
|
// dirName-derived (no runtime literal). getDirName('augment') resolves
|
||||||
content = content.replace(/~\/\.augment(?![\w-])/g, normalizedPathPrefix);
|
// to '.augment', so this is byte-identical to the prior hardcoded regexes.
|
||||||
content = content.replace(/\$HOME\/\.augment(?![\w-])/g, normalizedPathPrefix);
|
const _dd = escapeRegExp(dirName);
|
||||||
|
content = content.replace(new RegExp('~/' + _dd + '/', 'g'), pathPrefix);
|
||||||
|
content = content.replace(new RegExp('\\$HOME/' + _dd + '/', 'g'), pathPrefix);
|
||||||
|
content = content.replace(new RegExp('~/' + _dd + '(?![\\w-])', 'g'), normalizedPathPrefix);
|
||||||
|
content = content.replace(new RegExp('\\$HOME/' + _dd + '(?![\\w-])', 'g'), normalizedPathPrefix);
|
||||||
content = processAttribution(content, attribution);
|
content = processAttribution(content, attribution);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'trae':
|
case 'trae':
|
||||||
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
||||||
@@ -2631,8 +2641,11 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
|
|||||||
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
||||||
let content = fs.readFileSync(path.join(stagedDir, entry.name), 'utf8');
|
let content = fs.readFileSync(path.join(stagedDir, entry.name), 'utf8');
|
||||||
content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, attribution);
|
content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, attribution);
|
||||||
if (runtime === 'augment') {
|
// #2097 (ADR-1239): descriptor-driven — commandBodyConverter name comes
|
||||||
content = convertClaudeToAugmentMarkdown(content);
|
// from runtime.hostBehaviors instead of a hardcoded runtime-name branch.
|
||||||
|
const _cmdConv = _hostBehaviors(runtime).commandBodyConverter;
|
||||||
|
if (_cmdConv && COMMAND_BODY_CONVERTERS[_cmdConv]) {
|
||||||
|
content = COMMAND_BODY_CONVERTERS[_cmdConv](content);
|
||||||
}
|
}
|
||||||
fs.writeFileSync(path.join(tempDir, entry.name), content);
|
fs.writeFileSync(path.join(tempDir, entry.name), content);
|
||||||
}
|
}
|
||||||
|
|||||||
243
tests/augment-upgrades.test.cjs
Normal file
243
tests/augment-upgrades.test.cjs
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
'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');
|
||||||
|
});
|
||||||
202
tests/declarative-reference-augment.test.cjs
Normal file
202
tests/declarative-reference-augment.test.cjs
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
// allow-test-rule: structural-regression-guard — the descriptor-migration contract (no `runtime === 'augment'` / `isAugment`-as-conversion-branch dispatch logic left in the folded modules) is a property of the source text itself, so a source-grep is the only faithful check (#2097, mirrors tests/declarative-reference-antigravity.test.cjs #2096 AC2 guard).
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declarative reference host coverage — Augment Code (ADR-1239 / #2097
|
||||||
|
* EoS/augment migration).
|
||||||
|
*
|
||||||
|
* Augment is already a declarative-CLI host (nested-skill artifact layout,
|
||||||
|
* settings-json hook surface, Claude hook event dialect). #2097 folds its two
|
||||||
|
* remaining runtime-literal branches in src/runtime-artifact-conversion.cts
|
||||||
|
* onto descriptor-driven dispatch (runtime.hostBehaviors.commandBodyConverter)
|
||||||
|
* and deletes 2 dead-code sites in bin/install.js — mirroring the Antigravity
|
||||||
|
* migration (#2096) structure without duplicating its Antigravity-specific
|
||||||
|
* (GEMINI.md / reviewerCli / noPathRewrite) assertions.
|
||||||
|
*
|
||||||
|
* UPGRADE 3 (MCP companion, settings.json-hosted) live-install coverage is in
|
||||||
|
* tests/augment-upgrades.test.cjs — not duplicated here.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { test, before } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { execFileSync } = require('node:child_process');
|
||||||
|
|
||||||
|
const {
|
||||||
|
profileOf,
|
||||||
|
negotiateHostCapabilities,
|
||||||
|
UNDOCUMENTED,
|
||||||
|
} = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||||
|
const { createDeclarativeAdapter } = require('../gsd-core/bin/lib/adapter-declarative.cjs');
|
||||||
|
const { cleanup } = require('./helpers.cjs');
|
||||||
|
const { walk, runMinimalInstall, BUILD_SCRIPT } = require('./helpers/install-shared.cjs');
|
||||||
|
|
||||||
|
const DESC = path.join(__dirname, '..', 'capabilities', 'augment', 'capability.json');
|
||||||
|
const AUGMENT_CAP = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||||
|
const AUGMENT_AXES = AUGMENT_CAP.runtime.hostIntegration;
|
||||||
|
|
||||||
|
// hooks/dist is gitignored and built (mirrors golden-install-parity harness).
|
||||||
|
before(() => {
|
||||||
|
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Augment classifies as the declarative-cli profile (profileOf)', () => {
|
||||||
|
const desc = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||||
|
const axes = desc.runtime.hostIntegration;
|
||||||
|
assert.ok(axes && axes.embeddingMode, 'augment descriptor declares hostIntegration axes');
|
||||||
|
assert.equal(profileOf(axes), 'declarative-cli', 'Augment is a declarative-CLI host');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the public declarative adapter classifies Augment as a declarative host', () => {
|
||||||
|
const adapter = createDeclarativeAdapter({ runtime: 'augment' });
|
||||||
|
assert.equal(adapter.kind, 'declarative');
|
||||||
|
assert.equal(adapter.runtime, 'augment');
|
||||||
|
assert.equal(typeof adapter.install, 'function');
|
||||||
|
assert.equal(typeof adapter.uninstall, 'function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a real Augment install emits a gsd command/skill surface (invocable)', () => {
|
||||||
|
const { configDir, root } = runMinimalInstall({ runtime: 'augment', scope: 'global' });
|
||||||
|
try {
|
||||||
|
const files = walk(configDir);
|
||||||
|
assert.ok(files.length > 0, 'install must emit artifacts');
|
||||||
|
const gsdSurface = files.filter((f) => /gsd/i.test(path.relative(configDir, f)));
|
||||||
|
assert.ok(gsdSurface.length > 0,
|
||||||
|
'install must emit a gsd command/skill surface (declarative reference)');
|
||||||
|
} finally {
|
||||||
|
cleanup(root);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// #2097 EoS/augment — fail-closed negotiation + folded dispatch sub-axes
|
||||||
|
// (mirrors the antigravity/kimi reference tests).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('negotiateHostCapabilities never throws for augment, even fully corrupted', () => {
|
||||||
|
assert.doesNotThrow(() => negotiateHostCapabilities({}));
|
||||||
|
assert.doesNotThrow(() => negotiateHostCapabilities({ ...AUGMENT_AXES, embeddingMode: UNDOCUMENTED }));
|
||||||
|
assert.doesNotThrow(() => negotiateHostCapabilities({ ...AUGMENT_AXES, embeddingMode: 'future-unknown' }));
|
||||||
|
assert.doesNotThrow(() => negotiateHostCapabilities({ ...AUGMENT_AXES, dispatch: 'corrupted-not-an-object' }));
|
||||||
|
assert.doesNotThrow(() => negotiateHostCapabilities({ ...AUGMENT_AXES, dispatch: { ...AUGMENT_AXES.dispatch, maxDepth: 'not-a-number' } }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// AC-style proof: the 3 still-`undocumented` dispatch sub-axes (nested/
|
||||||
|
// maxDepth/backgroundDispatch) must degrade to the most-restrictive KNOWN
|
||||||
|
// value, never their optimistic value — mirrors antigravity's equivalent
|
||||||
|
// test, but Augment documents namedDispatch/background (unlike antigravity),
|
||||||
|
// so only 3 sub-axes are undocumented here, not 4. Real values confirmed via:
|
||||||
|
// node -e "const {negotiateHostCapabilities}=require('./gsd-core/bin/lib/host-integration.cjs');
|
||||||
|
// const cap=require('./capabilities/augment/capability.json');
|
||||||
|
// console.log(negotiateHostCapabilities(cap.runtime.hostIntegration).effective.dispatch)"
|
||||||
|
// -> { namedDispatch:true, nested:false, maxDepth:0, background:true, subagentToolkit:'full', backgroundDispatch:false }
|
||||||
|
test("augment's 3 still-undocumented dispatch sub-axes (nested/maxDepth/backgroundDispatch) degrade to the most-restrictive known value, not their optimistic value", () => {
|
||||||
|
// Sanity: the descriptor itself still declares these 3 as the undocumented
|
||||||
|
// sentinel, while namedDispatch/background/subagentToolkit are documented.
|
||||||
|
assert.equal(AUGMENT_AXES.dispatch.nested, 'undocumented');
|
||||||
|
assert.equal(AUGMENT_AXES.dispatch.maxDepth, 'undocumented');
|
||||||
|
assert.equal(AUGMENT_AXES.dispatch.backgroundDispatch, 'undocumented');
|
||||||
|
assert.equal(AUGMENT_AXES.dispatch.namedDispatch, true, 'sanity: namedDispatch is documented, not part of the undocumented set');
|
||||||
|
assert.equal(AUGMENT_AXES.dispatch.background, true, 'sanity: background is documented, not part of the undocumented set');
|
||||||
|
assert.equal(AUGMENT_AXES.dispatch.subagentToolkit, 'full', 'sanity: subagentToolkit is documented, not part of the undocumented set');
|
||||||
|
|
||||||
|
const { effective, warnings } = negotiateHostCapabilities(AUGMENT_AXES);
|
||||||
|
|
||||||
|
assert.equal(effective.dispatch.nested, false, 'undocumented nested must degrade to false, never true');
|
||||||
|
assert.equal(effective.dispatch.maxDepth, 0, 'undocumented maxDepth must degrade to 0, never -1/unbounded');
|
||||||
|
assert.equal(effective.dispatch.backgroundDispatch, false, 'undocumented backgroundDispatch must degrade to false, never true');
|
||||||
|
|
||||||
|
// namedDispatch/background/subagentToolkit are documented — they are
|
||||||
|
// trusted and survive negotiation unchanged, in contrast to the 3
|
||||||
|
// undocumented sub-axes above. Augment's negotiation must NOT flatten
|
||||||
|
// dispatch the way a fully-undocumented descriptor (e.g. antigravity's
|
||||||
|
// namedDispatch) would.
|
||||||
|
assert.equal(effective.dispatch.namedDispatch, true, "documented 'true' namedDispatch is trusted, unlike the undocumented sub-axes");
|
||||||
|
assert.equal(effective.dispatch.background, true, "documented 'true' background is trusted — not capped to false, since namedDispatch did not degrade closed");
|
||||||
|
assert.equal(effective.dispatch.subagentToolkit, 'full', "documented 'full' subagentToolkit is trusted, unlike the undocumented sub-axes");
|
||||||
|
|
||||||
|
for (const axis of ['nested', 'backgroundDispatch']) {
|
||||||
|
assert.ok(
|
||||||
|
warnings.some((w) => w.includes(`dispatch.${axis}`) && w.includes('undocumented')),
|
||||||
|
`a warning must be raised for the undocumented dispatch.${axis} axis`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert.ok(
|
||||||
|
warnings.some((w) => w.includes('dispatch.maxDepth')),
|
||||||
|
'a warning must be raised for the undocumented dispatch.maxDepth axis (reported as missing/non-number)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// #2097 AC2-equivalent — the hardcoded conversion-dispatch branches folded by
|
||||||
|
// this migration are retired from the folded modules.
|
||||||
|
//
|
||||||
|
// Unlike antigravity (#2096), where `isAntigravity` was eliminated from
|
||||||
|
// bin/install.js entirely, Augment's `isAugment` flag legitimately remains
|
||||||
|
// (destructured from runtimeFlags() and used for non-conversion verification
|
||||||
|
// logic — e.g. counting installed commands/ files). So this guard does NOT
|
||||||
|
// assert zero `isAugment` occurrences (that would be a false failure); it
|
||||||
|
// asserts no `isAugment` occurrence gates a call to an Augment *converter*
|
||||||
|
// function (the exact shape of the dead branch this migration deleted:
|
||||||
|
// `else if (isAugment) { content = convertClaudeAgentToAugmentAgent(content); }`).
|
||||||
|
// The switch `case 'augment':` label in _applyRuntimeRewrites is intentionally
|
||||||
|
// NOT flagged — it does plain dirName-derived path-prefix rewriting (same as
|
||||||
|
// the still-`case`-labeled trae/codebuddy/cursor/windsurf branches), not
|
||||||
|
// conversion-function dispatch, and antigravity's own guard tolerates its
|
||||||
|
// `case 'antigravity':` label for the identical reason (its regex only
|
||||||
|
// matches `runtime === 'x'`, never a `case` label).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
test('no `runtime === "augment"` string-equality branch remains in the descriptor-migrated modules', () => {
|
||||||
|
const strip = (src) => src
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||||
|
.replace(/\/\/[^\r\n]*/g, '')
|
||||||
|
.replace(/`[^`]*`/g, '');
|
||||||
|
const repoRoot = path.join(__dirname, '..');
|
||||||
|
const files = [
|
||||||
|
path.join(repoRoot, 'bin', 'install.js'),
|
||||||
|
path.join(repoRoot, 'src', 'runtime-artifact-conversion.cts'),
|
||||||
|
];
|
||||||
|
for (const file of files) {
|
||||||
|
const src = fs.readFileSync(file, 'utf8');
|
||||||
|
const stripped = strip(src);
|
||||||
|
const eqOffenders = stripped.match(/runtime\s*[!=]==\s*'augment'/g) || [];
|
||||||
|
assert.deepEqual(eqOffenders, [],
|
||||||
|
`no hardcoded runtime==='augment' branch may remain in ${path.relative(repoRoot, file)}; found: ${eqOffenders.join(', ')}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no `isAugment` occurrence gates a call to an Augment converter function (the deleted dead-code shape)', () => {
|
||||||
|
const converterNames = [
|
||||||
|
'convertClaudeAgentToAugmentAgent',
|
||||||
|
'convertClaudeToAugmentMarkdown',
|
||||||
|
'convertClaudeCommandToAugmentSkill',
|
||||||
|
'convertSlashCommandsToAugmentSkillMentions',
|
||||||
|
];
|
||||||
|
const converterCallPattern = new RegExp(`(${converterNames.join('|')})\\s*\\(`);
|
||||||
|
|
||||||
|
const file = path.join(__dirname, '..', 'bin', 'install.js');
|
||||||
|
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
||||||
|
const offenders = [];
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
// Only condition-position occurrences count (`if (isAugment)` / `else if
|
||||||
|
// (isAugment)`), not the runtimeFlags() destructure or unrelated reads.
|
||||||
|
if (!/(?:if|else if)\s*\(\s*isAugment\s*\)/.test(line)) continue;
|
||||||
|
const window = lines.slice(i, i + 4).join('\n');
|
||||||
|
if (converterCallPattern.test(window)) {
|
||||||
|
offenders.push(`line ${i + 1}: ${line.trim()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.deepEqual(offenders, [],
|
||||||
|
`no isAugment-gated call to an Augment converter may remain in bin/install.js; found: ${offenders.join(' | ')}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('legitimate isAugment destructure/enumeration sites survive (not eliminated, unlike isAntigravity)', () => {
|
||||||
|
const file = path.join(__dirname, '..', 'bin', 'install.js');
|
||||||
|
const src = fs.readFileSync(file, 'utf8');
|
||||||
|
assert.ok(/isAugment/.test(src), 'isAugment must still be destructured from runtimeFlags() for non-conversion uses');
|
||||||
|
assert.ok(/_DESCRIPTOR_AGENTS_RUNTIMES\s*=\s*new Set\(\[[^\]]*'augment'/.test(src),
|
||||||
|
'augment must remain in _DESCRIPTOR_AGENTS_RUNTIMES (descriptor-driven agent layout)');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user