* fix(#167): support query meta-command in gsd-tools * chore(#167): add changeset for query meta-command fix * fix(#167): pin claude runtime in local-agent regression tests * test(#3751): stabilize local-agent CI assertions
This commit is contained in:
5
.changeset/167-query-meta-command.md
Normal file
5
.changeset/167-query-meta-command.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 167
|
||||
---
|
||||
**`gsd-tools` query meta-command parity** — direct invocations like `node gsd-tools.cjs query init.progress` now behave the same as `node gsd-tools.cjs init.progress` instead of failing with `Unknown command: query`. This unblocks workflow preflight paths that call the CJS entrypoint directly with the `query` prefix.
|
||||
@@ -419,6 +419,13 @@ async function main() {
|
||||
|
||||
let command = args[0];
|
||||
|
||||
// Accept `query` meta-prefix parity with `gsd-sdk query ...`.
|
||||
// Workflows may call `node gsd-tools.cjs query <command>` directly.
|
||||
if (command === 'query') {
|
||||
args.shift();
|
||||
command = args[0];
|
||||
}
|
||||
|
||||
// #3243: accept dotted canonical form (e.g. `state.update`) as well as the
|
||||
// spaced form (`state update`). Workflow files and stale SDK binaries pass
|
||||
// the dotted canonical form directly; any caller that bypasses the SDK
|
||||
|
||||
20
tests/bug-167-query-meta-command.test.cjs
Normal file
20
tests/bug-167-query-meta-command.test.cjs
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { runGsdTools } = require('./helpers.cjs');
|
||||
|
||||
test('bug #167: query meta-command prefixes direct gsd-tools calls', () => {
|
||||
const direct = runGsdTools(['init.progress']);
|
||||
assert.equal(direct.success, true, `init.progress failed: ${direct.error || direct.output}`);
|
||||
|
||||
const meta = runGsdTools(['query', 'init.progress']);
|
||||
assert.equal(meta.success, true, `query init.progress failed: ${meta.error || meta.output}`);
|
||||
|
||||
assert.deepEqual(
|
||||
JSON.parse(meta.output),
|
||||
JSON.parse(direct.output),
|
||||
'query-prefixed and direct invocations should return identical init.progress payloads'
|
||||
);
|
||||
});
|
||||
@@ -30,6 +30,30 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const MODEL_PROFILES = require('../get-shit-done/bin/lib/model-profiles.cjs').MODEL_PROFILES;
|
||||
const EXPECTED_AGENTS = Object.keys(MODEL_PROFILES);
|
||||
|
||||
function writeRequiredAgents(agentsDir) {
|
||||
for (const agentName of EXPECTED_AGENTS) {
|
||||
fs.writeFileSync(
|
||||
path.join(agentsDir, `${agentName}.md`),
|
||||
`---\nname: ${agentName}\ndescription: test\ntools: Read\n---\nAgent content.\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePathForAssert(targetPath) {
|
||||
if (typeof targetPath !== 'string') return targetPath;
|
||||
const resolved = path.resolve(targetPath);
|
||||
try {
|
||||
if (typeof fs.realpathSync.native === 'function') {
|
||||
return fs.realpathSync.native(resolved);
|
||||
}
|
||||
return fs.realpathSync(resolved);
|
||||
} catch {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Source-file structural assertions (no build required) ───────────────────
|
||||
|
||||
@@ -152,11 +176,14 @@ describe('#3751: resolveAgentsDir() repo-local fallback — runtime behaviour',
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3751-'));
|
||||
savedEnv = {
|
||||
GSD_AGENTS_DIR: process.env.GSD_AGENTS_DIR,
|
||||
GSD_RUNTIME: process.env.GSD_RUNTIME,
|
||||
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
|
||||
HOME: process.env.HOME,
|
||||
};
|
||||
// Clear explicit overrides so we exercise the fallback path
|
||||
delete process.env.GSD_AGENTS_DIR;
|
||||
// Make runtime deterministic: this suite validates Claude local-agent semantics.
|
||||
process.env.GSD_RUNTIME = 'claude';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -167,6 +194,11 @@ describe('#3751: resolveAgentsDir() repo-local fallback — runtime behaviour',
|
||||
} else {
|
||||
delete process.env.GSD_AGENTS_DIR;
|
||||
}
|
||||
if (savedEnv.GSD_RUNTIME !== undefined) {
|
||||
process.env.GSD_RUNTIME = savedEnv.GSD_RUNTIME;
|
||||
} else {
|
||||
delete process.env.GSD_RUNTIME;
|
||||
}
|
||||
if (savedEnv.CLAUDE_CONFIG_DIR !== undefined) {
|
||||
process.env.CLAUDE_CONFIG_DIR = savedEnv.CLAUDE_CONFIG_DIR;
|
||||
} else {
|
||||
@@ -187,14 +219,12 @@ describe('#3751: resolveAgentsDir() repo-local fallback — runtime behaviour',
|
||||
// DO NOT create fakeGlobalConfig/agents/ — simulates absent global agents
|
||||
process.env.CLAUDE_CONFIG_DIR = fakeGlobalConfig;
|
||||
|
||||
// Set up repo-local .claude/agents with a GSD agent definition
|
||||
// Set up repo-local .claude/agents with all required GSD agent files.
|
||||
// `agents_installed` is only true when every MODEL_PROFILES key exists.
|
||||
const repoRoot = path.join(tmpDir, 'repo');
|
||||
const repoLocalAgentsDir = path.join(repoRoot, '.claude', 'agents');
|
||||
fs.mkdirSync(repoLocalAgentsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(repoLocalAgentsDir, 'gsd-project-researcher.md'),
|
||||
'---\nname: gsd-project-researcher\ndescription: test\ntools: Read\n---\nAgent content.\n',
|
||||
);
|
||||
writeRequiredAgents(repoLocalAgentsDir);
|
||||
|
||||
// Dynamically require helpers so CLAUDE_CONFIG_DIR is picked up
|
||||
// (Node caches modules, so we clear the cache first)
|
||||
@@ -314,15 +344,20 @@ describe('#3799: resolveAgentsDir() local-first resolution — runtime behaviour
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3799-'));
|
||||
savedEnv = {
|
||||
GSD_AGENTS_DIR: process.env.GSD_AGENTS_DIR,
|
||||
GSD_RUNTIME: process.env.GSD_RUNTIME,
|
||||
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
|
||||
};
|
||||
delete process.env.GSD_AGENTS_DIR;
|
||||
// Keep runtime fixed across suite-order changes and leaked test env.
|
||||
process.env.GSD_RUNTIME = 'claude';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
if (savedEnv.GSD_AGENTS_DIR !== undefined) process.env.GSD_AGENTS_DIR = savedEnv.GSD_AGENTS_DIR;
|
||||
else delete process.env.GSD_AGENTS_DIR;
|
||||
if (savedEnv.GSD_RUNTIME !== undefined) process.env.GSD_RUNTIME = savedEnv.GSD_RUNTIME;
|
||||
else delete process.env.GSD_RUNTIME;
|
||||
if (savedEnv.CLAUDE_CONFIG_DIR !== undefined) process.env.CLAUDE_CONFIG_DIR = savedEnv.CLAUDE_CONFIG_DIR;
|
||||
else delete process.env.CLAUDE_CONFIG_DIR;
|
||||
});
|
||||
@@ -346,14 +381,11 @@ describe('#3799: resolveAgentsDir() local-first resolution — runtime behaviour
|
||||
// global agents/ exists but is EMPTY — simulates Claude auto-creating the dir
|
||||
process.env.CLAUDE_CONFIG_DIR = fakeGlobalConfig;
|
||||
|
||||
// Set up project-local .claude/agents with a GSD agent definition
|
||||
// Set up project-local .claude/agents with all required GSD agent files
|
||||
const repoRoot = path.join(tmpDir, 'repo');
|
||||
const repoLocalAgentsDir = path.join(repoRoot, '.claude', 'agents');
|
||||
fs.mkdirSync(repoLocalAgentsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(repoLocalAgentsDir, 'gsd-project-researcher.md'),
|
||||
'---\nname: gsd-project-researcher\ndescription: test\ntools: Read\n---\nAgent content.\n',
|
||||
);
|
||||
writeRequiredAgents(repoLocalAgentsDir);
|
||||
|
||||
const result = runGsdTools(
|
||||
['query', 'init.new-project', '--raw'],
|
||||
@@ -369,8 +401,8 @@ describe('#3799: resolveAgentsDir() local-first resolution — runtime behaviour
|
||||
if (parsed && typeof parsed.agents_dir !== 'undefined') {
|
||||
// agents_dir must point to the local dir, not the empty global dir
|
||||
assert.strictEqual(
|
||||
parsed.agents_dir,
|
||||
repoLocalAgentsDir,
|
||||
normalizePathForAssert(parsed.agents_dir),
|
||||
normalizePathForAssert(repoLocalAgentsDir),
|
||||
`agents_dir must resolve to project-local path, got: ${parsed.agents_dir} (#3799)`,
|
||||
);
|
||||
}
|
||||
@@ -410,8 +442,8 @@ describe('#3799: resolveAgentsDir() local-first resolution — runtime behaviour
|
||||
try { parsed = JSON.parse(result.output); } catch { return; }
|
||||
if (parsed && typeof parsed.agents_dir !== 'undefined') {
|
||||
assert.strictEqual(
|
||||
parsed.agents_dir,
|
||||
fakeGlobalAgents,
|
||||
normalizePathForAssert(parsed.agents_dir),
|
||||
normalizePathForAssert(fakeGlobalAgents),
|
||||
`agents_dir must resolve to global path when no local dir exists, got: ${parsed.agents_dir} (#3799)`,
|
||||
);
|
||||
}
|
||||
@@ -426,13 +458,13 @@ describe('#3799: resolveAgentsDir() local-first resolution — runtime behaviour
|
||||
const fakeGlobalConfig = path.join(tmpDir, 'fake-global-both');
|
||||
const fakeGlobalAgents = path.join(fakeGlobalConfig, 'agents');
|
||||
fs.mkdirSync(fakeGlobalAgents, { recursive: true });
|
||||
fs.writeFileSync(path.join(fakeGlobalAgents, 'gsd-project-researcher.md'), '# global agent\n');
|
||||
writeRequiredAgents(fakeGlobalAgents);
|
||||
process.env.CLAUDE_CONFIG_DIR = fakeGlobalConfig;
|
||||
|
||||
const repoRoot = path.join(tmpDir, 'repo-both');
|
||||
const repoLocalAgentsDir = path.join(repoRoot, '.claude', 'agents');
|
||||
fs.mkdirSync(repoLocalAgentsDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(repoLocalAgentsDir, 'gsd-project-researcher.md'), '# local agent\n');
|
||||
writeRequiredAgents(repoLocalAgentsDir);
|
||||
|
||||
const result = runGsdTools(
|
||||
['query', 'init.new-project', '--raw'],
|
||||
@@ -447,8 +479,8 @@ describe('#3799: resolveAgentsDir() local-first resolution — runtime behaviour
|
||||
try { parsed = JSON.parse(result.output); } catch { return; }
|
||||
if (parsed && typeof parsed.agents_dir !== 'undefined') {
|
||||
assert.strictEqual(
|
||||
parsed.agents_dir,
|
||||
repoLocalAgentsDir,
|
||||
normalizePathForAssert(parsed.agents_dir),
|
||||
normalizePathForAssert(repoLocalAgentsDir),
|
||||
`agents_dir must resolve to local path when both exist, got: ${parsed.agents_dir} (#3799)`,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user