mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
fix(factory-plugin): pin CLI version and parse quoted shell patterns
- Pin mcp.json and the hook's npx fallback to gitnexus@<version> from the plugin manifest, registered with the release sync script so a mutable @latest can never execute on MCP connect or augment fallback - Port the #2938 shell tokenizer (tokenizeShellWords + parseRgGrepPattern) so quoted, backslash-escaped, --regexp=, -eVALUE, and -- patterns survive - Add the #2938 regression matrix and pin assertions to factory-plugin.test.ts
This commit is contained in:
parent
a262dfbaaa
commit
848ed2a0b6
5 changed files with 238 additions and 97 deletions
|
|
@ -1,23 +1,17 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* GitNexus Factory AI (Droid) Plugin Hook
|
||||
* GitNexus Factory AI (Droid) plugin hook.
|
||||
*
|
||||
* PostToolUse — augments Grep/Glob/Execute searches with graph context from
|
||||
* the GitNexus index and returns it via hookSpecificOutput.additionalContext.
|
||||
* PostToolUse — augments Grep/Glob/Execute searches with graph context and
|
||||
* returns it via hookSpecificOutput.additionalContext.
|
||||
*
|
||||
* Reuses the same guards as the Claude/Codex adapter (bundled byte-identical,
|
||||
* kept in lockstep by test/unit/factory-plugin.test.ts):
|
||||
* - acquireHookSlot — per-repo cap on concurrent augment children so
|
||||
* parallel sessions can't fan out unbounded `gitnexus augment` spawns
|
||||
* (#1486).
|
||||
* - LadybugDB owner probe — skips the CLI augment when a GitNexus MCP/serve
|
||||
* process already holds the single-writer DB lock, avoiding contention
|
||||
* (#2396); the in-session MCP tools cover augmentation instead.
|
||||
* Reuses the Claude adapter's guards, bundled byte-identical: acquireHookSlot
|
||||
* caps concurrent augment children per repo (#1486), and the LadybugDB owner
|
||||
* probe skips the CLI augment when an MCP/serve process already holds the
|
||||
* single-writer lock (#2396).
|
||||
*
|
||||
* The augment CLI child is NOT wrapped in the coreutils `timeout`
|
||||
* orphan-containment guard the full Claude adapter uses (#2163) — same scope
|
||||
* as the Cursor integration. Add it (resolveUnixGuardTimeout is exported by
|
||||
* the bundled probe) if orphaned augment children become a problem here.
|
||||
* The augment child is not wrapped in the coreutils `timeout` orphan guard the
|
||||
* full Claude adapter uses (#2163) — same scope as the Cursor integration.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
|
|
@ -26,22 +20,22 @@ const { spawnSync } = require('child_process');
|
|||
const { acquireHookSlot } = require('./hook-lock.js');
|
||||
const { hasGitNexusDbLockedByGitNexusServer } = require('./hook-db-lock-probe.cjs');
|
||||
|
||||
/**
|
||||
* Read JSON input from stdin synchronously.
|
||||
*/
|
||||
// Pin the CLI instead of tracking `latest`: npm versions are immutable, so only
|
||||
// a plugin revision can change what the fallback below executes. The release
|
||||
// stamps this manifest (gitnexus/scripts/sync-plugin-manifests.mjs).
|
||||
const { version: PINNED_VERSION } = require('../.factory-plugin/plugin.json');
|
||||
|
||||
function readInput() {
|
||||
try {
|
||||
const data = fs.readFileSync(0, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
return JSON.parse(fs.readFileSync(0, 'utf-8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A `.gitnexus/` that holds `registry.json`/`repos` (and no per-repo index
|
||||
* metadata) is the global registry, not a repo index — never augment against
|
||||
* it. Mirrors the Claude adapter's guard.
|
||||
* A `.gitnexus/` holding `registry.json`/`repos` (and no per-repo index
|
||||
* metadata) is the global registry, not a repo index — never augment against it.
|
||||
*/
|
||||
function isGlobalRegistryDir(candidate) {
|
||||
if (
|
||||
|
|
@ -56,10 +50,7 @@ function isGlobalRegistryDir(candidate) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk up from startDir looking for a non-registry `.gitnexus/` folder. Returns
|
||||
* the path to `.gitnexus/` or null if not found within 5 levels.
|
||||
*/
|
||||
/** Walk up from startDir for a non-registry `.gitnexus/`, at most 5 levels. */
|
||||
function findGitNexusDir(startDir) {
|
||||
let dir = startDir || process.cwd();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
|
|
@ -73,9 +64,138 @@ function findGitNexusDir(startDir) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Extract a search pattern from a Factory tool payload. Factory's shell tool is
|
||||
* `Execute` (Claude's is `Bash`); Grep/Glob match Claude's.
|
||||
* Split a command the way a POSIX shell would, so quoted and backslash-escaped
|
||||
* patterns survive as one token. Kept identical to the Cursor adapter's
|
||||
* tokenizer (#2938) so the two can collapse into a shared module later.
|
||||
*/
|
||||
function tokenizeShellWords(command) {
|
||||
const tokens = [];
|
||||
let current = '';
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
let hasToken = false;
|
||||
|
||||
for (let index = 0; index < command.length; index += 1) {
|
||||
const char = command[index];
|
||||
if (escaped) {
|
||||
current += char;
|
||||
escaped = false;
|
||||
hasToken = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (quote === "'") {
|
||||
if (char === "'") quote = null;
|
||||
else current += char;
|
||||
hasToken = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (quote === '"') {
|
||||
if (char === '"') {
|
||||
quote = null;
|
||||
} else if (char === '\\') {
|
||||
const next = command[index + 1];
|
||||
// Inside double quotes a backslash only escapes these four; otherwise
|
||||
// it is a literal character (so Windows paths survive intact).
|
||||
if (next === '$' || next === '`' || next === '"' || next === '\\') {
|
||||
escaped = true;
|
||||
} else {
|
||||
current += '\\';
|
||||
}
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
hasToken = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
escaped = true;
|
||||
hasToken = true;
|
||||
} else if (char === "'" || char === '"') {
|
||||
quote = char;
|
||||
hasToken = true;
|
||||
} else if (/\s/.test(char)) {
|
||||
if (hasToken) tokens.push(current);
|
||||
current = '';
|
||||
hasToken = false;
|
||||
} else {
|
||||
current += char;
|
||||
hasToken = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (escaped) current += '\\';
|
||||
if (hasToken) tokens.push(current);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/** Recover the search pattern from an `rg`/`grep` command line. */
|
||||
function parseRgGrepPattern(cmd) {
|
||||
const tokens = tokenizeShellWords(cmd);
|
||||
let foundCmd = false;
|
||||
let skipNext = false;
|
||||
let skipNextAsPattern = false;
|
||||
let endOfOptions = false;
|
||||
const flagsWithValues = new Set([
|
||||
'-e',
|
||||
'-f',
|
||||
'-m',
|
||||
'-A',
|
||||
'-B',
|
||||
'-C',
|
||||
'-g',
|
||||
'--glob',
|
||||
'-t',
|
||||
'--type',
|
||||
'--include',
|
||||
'--exclude',
|
||||
]);
|
||||
const patternFlags = new Set(['-e', '--regexp']);
|
||||
|
||||
for (const token of tokens) {
|
||||
if (skipNext) {
|
||||
skipNext = false;
|
||||
if (skipNextAsPattern) {
|
||||
return token.length >= 3 ? token : null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!foundCmd) {
|
||||
// Match on the basename so absolute paths (`/usr/bin/rg`) and Windows
|
||||
// `rg.exe` count as the command.
|
||||
const commandName = token
|
||||
.split(/[\\/]/)
|
||||
.pop()
|
||||
?.replace(/\.exe$/i, '');
|
||||
if (commandName === 'rg' || commandName === 'grep') foundCmd = true;
|
||||
continue;
|
||||
}
|
||||
if (endOfOptions) {
|
||||
return token.length >= 3 ? token : null;
|
||||
}
|
||||
if (token === '--') {
|
||||
endOfOptions = true;
|
||||
continue;
|
||||
}
|
||||
if (token.startsWith('-')) {
|
||||
const attachedPattern = token.match(/^--regexp=(.+)$/) || token.match(/^-e(.+)$/);
|
||||
if (attachedPattern) {
|
||||
return attachedPattern[1].length >= 3 ? attachedPattern[1] : null;
|
||||
}
|
||||
if (flagsWithValues.has(token) || patternFlags.has(token)) {
|
||||
skipNext = true;
|
||||
skipNextAsPattern = patternFlags.has(token);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return token.length >= 3 ? token : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Factory's shell tool is `Execute` (Claude's is `Bash`); Grep/Glob match Claude's. */
|
||||
function extractPattern(toolName, toolInput) {
|
||||
if (toolName === 'Grep') {
|
||||
return toolInput.pattern || null;
|
||||
|
|
@ -90,67 +210,24 @@ function extractPattern(toolName, toolInput) {
|
|||
if (toolName === 'Execute') {
|
||||
const cmd = toolInput.command || '';
|
||||
if (!/\brg\b|\bgrep\b/.test(cmd)) return null;
|
||||
|
||||
// NOTE: split(/\s+/) cannot handle shell quoting, same as the Cursor
|
||||
// integration. `rg "User Service" src/` yields "User" (first token after
|
||||
// rg/grep, quotes stripped) rather than the full phrase — BM25 is already
|
||||
// token-tolerant, so the multi-word pattern is deliberately not
|
||||
// reconstructed. Worst case is augment context for a narrower term than
|
||||
// the agent searched. Quoted single tokens (`rg "validateUser"`) are exact.
|
||||
const tokens = cmd.split(/\s+/);
|
||||
let foundCmd = false;
|
||||
let skipNext = false;
|
||||
const flagsWithValues = new Set([
|
||||
'-e',
|
||||
'-f',
|
||||
'-m',
|
||||
'-A',
|
||||
'-B',
|
||||
'-C',
|
||||
'-g',
|
||||
'--glob',
|
||||
'-t',
|
||||
'--type',
|
||||
'--include',
|
||||
'--exclude',
|
||||
]);
|
||||
|
||||
for (const token of tokens) {
|
||||
if (skipNext) {
|
||||
skipNext = false;
|
||||
continue;
|
||||
}
|
||||
if (!foundCmd) {
|
||||
if (/\brg\b|\bgrep\b/.test(token)) foundCmd = true;
|
||||
continue;
|
||||
}
|
||||
if (token.startsWith('-')) {
|
||||
if (flagsWithValues.has(token)) skipNext = true;
|
||||
continue;
|
||||
}
|
||||
const cleaned = token.replace(/['"]/g, '');
|
||||
return cleaned.length >= 3 ? cleaned : null;
|
||||
}
|
||||
return null;
|
||||
return parseRgGrepPattern(cmd);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `gitnexus augment` for `pattern` and return its stderr (the augment CLI
|
||||
* writes results to stderr; LadybugDB's native module captures stdout at the OS
|
||||
* fd level, making it unusable in subprocess contexts). Tries a PATH-installed
|
||||
* binary first, then falls back to npx.
|
||||
* Run `gitnexus augment` for `pattern` and return its stderr — the augment CLI
|
||||
* writes results there because LadybugDB's native module captures stdout at the
|
||||
* OS fd level.
|
||||
*
|
||||
* Honors GITNEXUS_HOOK_CLI_PATH first, same as the Claude adapter: it runs the
|
||||
* CLI as `node <path>`, which is the only branch that works on Windows, where
|
||||
* Node refuses to spawn the `.cmd` launcher shims below without a shell
|
||||
* (CVE-2024-27980). Falls back to a PATH binary, then npx.
|
||||
* GITNEXUS_HOOK_CLI_PATH is tried first and run as `node <path>`, the only form
|
||||
* that works on Windows, where Node refuses to spawn the `.cmd` shims without a
|
||||
* shell (CVE-2024-27980). Then a PATH binary, then a version-pinned npx.
|
||||
*
|
||||
* SECURITY: `pattern` is passed after the `--` end-of-options marker and never
|
||||
* through a shell — the Windows npx fallback invokes `npx.cmd` directly rather
|
||||
* than `shell: true`, so a pattern like `-rf` or `$(...)` is inert.
|
||||
* SECURITY: `pattern` follows the `--` end-of-options marker and never reaches a
|
||||
* shell (the Windows fallback invokes `npx.cmd` directly rather than
|
||||
* `shell: true`), so `-rf` or `$(...)` is inert.
|
||||
*/
|
||||
function runAugment(pattern, cwd) {
|
||||
const isWin = process.platform === 'win32';
|
||||
|
|
@ -186,7 +263,11 @@ function runAugment(pattern, cwd) {
|
|||
}
|
||||
|
||||
try {
|
||||
const child = spawnSync(isWin ? 'npx.cmd' : 'npx', ['-y', 'gitnexus', ...args], spawnOpts);
|
||||
const child = spawnSync(
|
||||
isWin ? 'npx.cmd' : 'npx',
|
||||
['-y', `gitnexus@${PINNED_VERSION}`, ...args],
|
||||
spawnOpts,
|
||||
);
|
||||
if (!child.error && child.status === 0 && child.stderr && child.stderr.trim()) {
|
||||
return child.stderr;
|
||||
}
|
||||
|
|
@ -219,9 +300,9 @@ function main() {
|
|||
let result = '';
|
||||
try {
|
||||
if (hasGitNexusDbLockedByGitNexusServer(path.join(gitNexusDir, 'lbug'), process.pid)) {
|
||||
// #2396: a GitNexus MCP/serve process owns the single-writer DB, so a
|
||||
// competing CLI augment would only contend on the lock. The session's
|
||||
// MCP tools cover augmentation instead — skip silently.
|
||||
// #2396: an MCP/serve process owns the single-writer DB, so a competing
|
||||
// CLI augment would only contend on the lock. Its MCP tools cover
|
||||
// augmentation instead — skip silently.
|
||||
return;
|
||||
}
|
||||
result = runAugment(pattern, cwd);
|
||||
|
|
@ -246,4 +327,6 @@ function main() {
|
|||
}
|
||||
}
|
||||
|
||||
main();
|
||||
if (require.main === module) main();
|
||||
|
||||
module.exports = { parseRgGrepPattern, tokenizeShellWords };
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"mcpServers": {
|
||||
"gitnexus": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "gitnexus@latest", "mcp"]
|
||||
"args": ["-y", "gitnexus@1.6.9", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
* - gitnexus-claude-plugin/.codex-plugin/plugin.json (top-level version)
|
||||
* - .agents/plugins/marketplace.json (plugins[gitnexus])
|
||||
* - gitnexus-claude-plugin/skills/<skill>/mcp.json (gitnexus@<version> launch arg, x10)
|
||||
* - gitnexus-factory-plugin/.factory-plugin/plugin.json (top-level version)
|
||||
* - gitnexus-factory-plugin/mcp.json (gitnexus@<version> launch arg)
|
||||
*
|
||||
* Modes:
|
||||
* node scripts/sync-plugin-manifests.mjs rewrite stale surfaces
|
||||
|
|
@ -54,6 +56,12 @@ const MANIFEST_SURFACES = [
|
|||
file: `gitnexus-claude-plugin/skills/${name}/mcp.json`,
|
||||
kind: 'mcp',
|
||||
})),
|
||||
// The Factory plugin's manifest is also its pin source at runtime: the
|
||||
// PostToolUse hook reads this version to build its `npx -y gitnexus@<version>`
|
||||
// fallback, so stamping it here keeps the hook and the MCP entry on the same
|
||||
// released CLI.
|
||||
{ file: 'gitnexus-factory-plugin/.factory-plugin/plugin.json', kind: 'plugin' },
|
||||
{ file: 'gitnexus-factory-plugin/mcp.json', kind: 'mcp' },
|
||||
];
|
||||
|
||||
const PLUGIN_NAME = 'gitnexus';
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { createRequire } from 'module';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
|
@ -38,6 +39,11 @@ const CLAUDE_HOOKS = path.join(REPO_ROOT, 'gitnexus-claude-plugin', 'hooks');
|
|||
// canonical Claude-adapter copies.
|
||||
const BUNDLED_GUARDS = ['hook-lock.js', 'hook-db-lock-probe.cjs', 'win-rm-list-json.ps1'] as const;
|
||||
|
||||
const require_ = createRequire(import.meta.url);
|
||||
const { parseRgGrepPattern } = require_(HOOK) as {
|
||||
parseRgGrepPattern: (command: string) => string | null;
|
||||
};
|
||||
|
||||
// ─── Manifest / file presence ───────────────────────────────────────
|
||||
|
||||
describe('Factory plugin files', () => {
|
||||
|
|
@ -96,6 +102,16 @@ describe('Factory mcp.json', () => {
|
|||
expect(mcp.mcpServers?.gitnexus?.command).toBe('npx');
|
||||
expect(mcp.mcpServers.gitnexus.args).toContain('mcp');
|
||||
});
|
||||
|
||||
// Executed state, not quickstart docs: `@latest` would let a future registry
|
||||
// upload run on MCP connect without a plugin revision.
|
||||
it('pins the CLI to the released version instead of a mutable tag', () => {
|
||||
const pkg = JSON.parse(
|
||||
fs.readFileSync(path.join(REPO_ROOT, 'gitnexus', 'package.json'), 'utf-8'),
|
||||
);
|
||||
expect(mcp.mcpServers.gitnexus.args).toContain(`gitnexus@${pkg.version}`);
|
||||
expect(JSON.stringify(mcp)).not.toContain('gitnexus@latest');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── hooks.json wiring ──────────────────────────────────────────────
|
||||
|
|
@ -155,6 +171,14 @@ describe('Factory hook source regressions', () => {
|
|||
expect(source).toContain('npx.cmd');
|
||||
});
|
||||
|
||||
// The npx fallback runs on ordinary tool calls whenever the CLI is not on
|
||||
// PATH, so an unpinned ref would execute whatever currently owns the tag.
|
||||
it('pins the npx fallback to the manifest version, never a mutable tag', () => {
|
||||
expect(source).not.toContain('gitnexus@latest');
|
||||
expect(source).toContain("require('../.factory-plugin/plugin.json')");
|
||||
expect(source).toContain('`gitnexus@${PINNED_VERSION}`');
|
||||
});
|
||||
|
||||
// Windows regression: Node refuses to spawn the .cmd launcher shims without a
|
||||
// shell (CVE-2024-27980), so `node <cliPath>` is the only branch that runs
|
||||
// there. Same escape hatch the Claude adapter honors.
|
||||
|
|
@ -182,6 +206,28 @@ describe('Factory hook source regressions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ─── Execute pattern parsing (shares the Cursor adapter's #2938 matrix) ─
|
||||
|
||||
describe('Factory Execute pattern parser', () => {
|
||||
it.each([
|
||||
['rg "User Service" src/', 'User Service'],
|
||||
["grep 'error boundary' -- src/", 'error boundary'],
|
||||
['rg User\\ Service src/', 'User Service'],
|
||||
[String.raw`rg "C:\Users" src/`, String.raw`C:\Users`],
|
||||
['rg -e "User Service" src/', 'User Service'],
|
||||
['rg --regexp=UserService src/', 'UserService'],
|
||||
['grep -eUserService src/', 'UserService'],
|
||||
['/usr/bin/rg -- "User Service" src/', 'User Service'],
|
||||
['rg -- -error src/', '-error'],
|
||||
['rg "validateUser"', 'validateUser'],
|
||||
['rg -t ts UserService src/', 'UserService'],
|
||||
['rg --glob "*.ts" UserService', 'UserService'],
|
||||
['rg ab src/', null],
|
||||
])('extracts %j from %j', (command, expected) => {
|
||||
expect(parseRgGrepPattern(command)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Behavior: early-exit paths (no augment spawned) ────────────────
|
||||
|
||||
describe('Factory hook behavior — early exits', () => {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ const SURFACES = [
|
|||
'.claude-plugin/marketplace.json',
|
||||
'gitnexus-claude-plugin/.codex-plugin/plugin.json',
|
||||
'.agents/plugins/marketplace.json',
|
||||
'gitnexus-factory-plugin/.factory-plugin/plugin.json',
|
||||
] as const;
|
||||
|
||||
const FACTORY_MCP = 'gitnexus-factory-plugin/mcp.json';
|
||||
|
||||
const MCP_SKILL_DIRS = [
|
||||
'gitnexus-plan',
|
||||
'gitnexus-work',
|
||||
|
|
@ -24,7 +27,7 @@ const MCP_SKILL_DIRS = [
|
|||
'gitnexus-refactoring',
|
||||
] as const;
|
||||
|
||||
const TOTAL_SURFACES = SURFACES.length + MCP_SKILL_DIRS.length;
|
||||
const TOTAL_SURFACES = SURFACES.length + MCP_SKILL_DIRS.length + 1;
|
||||
|
||||
function mcpPath(dir: string): string {
|
||||
return `gitnexus-claude-plugin/skills/${dir}/mcp.json`;
|
||||
|
|
@ -56,8 +59,9 @@ function makeRoot(packageVersion: string, manifestVersion: string): string {
|
|||
name: 'gitnexus-marketplace',
|
||||
plugins: [{ name: 'gitnexus', version: manifestVersion, category: 'Developer Tools' }],
|
||||
});
|
||||
for (const dir of MCP_SKILL_DIRS) {
|
||||
writeJson(root, mcpPath(dir), {
|
||||
writeJson(root, SURFACES[4], { name: 'gitnexus', version: manifestVersion });
|
||||
for (const dir of [...MCP_SKILL_DIRS.map(mcpPath), FACTORY_MCP]) {
|
||||
writeJson(root, dir, {
|
||||
mcpServers: {
|
||||
gitnexus: { command: 'npx', args: ['-y', `gitnexus@${manifestVersion}`, 'mcp'] },
|
||||
},
|
||||
|
|
@ -89,19 +93,19 @@ describe('syncPluginManifests (#2445)', () => {
|
|||
expect(result.version).toBe('1.6.10-rc.29');
|
||||
expect(result.synced).toHaveLength(TOTAL_SURFACES);
|
||||
expect(result.stale.map(({ from }) => from)).toEqual(Array(TOTAL_SURFACES).fill('1.6.9'));
|
||||
expect(readVersions(root)).toEqual(Array(4).fill('1.6.10-rc.29'));
|
||||
expect(readVersions(root)).toEqual(Array(SURFACES.length).fill('1.6.10-rc.29'));
|
||||
});
|
||||
|
||||
it('pins the gitnexus@<version> launch arg in every plugin skill mcp.json', () => {
|
||||
it('pins the gitnexus@<version> launch arg in every executable mcp.json', () => {
|
||||
const root = makeRoot('1.6.10-rc.29', '1.6.9');
|
||||
|
||||
syncPluginManifests(root);
|
||||
|
||||
for (const dir of MCP_SKILL_DIRS) {
|
||||
const mcp = JSON.parse(readFileSync(path.join(root, mcpPath(dir)), 'utf8')) as {
|
||||
for (const file of [...MCP_SKILL_DIRS.map(mcpPath), FACTORY_MCP]) {
|
||||
const mcp = JSON.parse(readFileSync(path.join(root, file), 'utf8')) as {
|
||||
mcpServers: { gitnexus: { args: string[] } };
|
||||
};
|
||||
expect(mcp.mcpServers.gitnexus.args).toEqual(['-y', 'gitnexus@1.6.10-rc.29', 'mcp']);
|
||||
expect(mcp.mcpServers.gitnexus.args, file).toEqual(['-y', 'gitnexus@1.6.10-rc.29', 'mcp']);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -131,7 +135,7 @@ describe('syncPluginManifests (#2445)', () => {
|
|||
|
||||
expect(result.stale).toHaveLength(TOTAL_SURFACES);
|
||||
expect(result.synced).toHaveLength(0);
|
||||
expect(readVersions(root)).toEqual(Array(4).fill('1.6.9'));
|
||||
expect(readVersions(root)).toEqual(Array(SURFACES.length).fill('1.6.9'));
|
||||
});
|
||||
|
||||
it('changes only the version text and preserves the surrounding formatting', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue