Files
msd-core/tests/qwen-upgrades.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

153 lines
7.3 KiB
JavaScript

'use strict';
/**
* qwen capability UPGRADES — ADR-1239 Phase D / #2092 (EoS/qwen).
*
* Drives the user-reachable surface (spawned `bin/install.js` via
* `runMinimalInstall`) to prove the two real upgrades qwen contributes beyond
* the hermes migration (#2091):
*
* UPGRADE 1 — native `agents` artifact-layout kind: MSD specialist agents
* project into `<configDir>/agents/msd-*.md` as native Qwen subagents
* (`name:`/`description:`/`tools:` YAML-block frontmatter — Qwen's own
* subagent schema, converted by `convertClaudeAgentToQwenAgent`).
*
* UPGRADE 2 — `SubagentStart` hook: wired symmetrically with the existing
* `SubagentStop` hook (same command + timeout), and proven to be
* descriptor-gated — a runtime whose `extendedHookEvents` omits
* `SubagentStart` (e.g. claude) must NOT get the hook, even though it does
* get `SubagentStop`.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { runMinimalInstall } = require('./helpers/install-shared.cjs');
const { cleanup } = require('./helpers.cjs');
const { listAgentFiles } = require('./helpers/agent-roster.cjs');
const QWEN_CAP = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'qwen', 'capability.json'), 'utf8'),
);
/** Extract the YAML frontmatter block (between the first pair of `---` lines), or null. */
function parseFrontmatter(content) {
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
return m ? m[1] : null;
}
// -- UPGRADE 1: native agents artifact-layout kind ---------------------------
for (const scope of ['global', 'local']) {
test(`qwen --${scope}: native .qwen/agents/*.md subagent projection (UPGRADE 1)`, (t) => {
const { configDir, root } = runMinimalInstall({ runtime: 'qwen', scope });
t.after(() => cleanup(root));
const agentsDir = path.join(configDir, 'agents');
assert.ok(fs.existsSync(agentsDir), `${agentsDir} must exist`);
const expectedNames = listAgentFiles(); // dynamically derived source roster
assert.equal(expectedNames.length, 35,
'sanity: shipped MSD agent roster is 35 files — update this boundary if the roster changes');
const installedFiles = fs.readdirSync(agentsDir)
.filter((f) => f.startsWith('msd-') && f.endsWith('.md'));
// Boundary-safe: at least the full expected roster, AND every expected
// name present by NAME (not just count) — a mis-copy that drops one
// agent while adding a stray file would still satisfy a bare >= count.
assert.ok(installedFiles.length >= expectedNames.length,
`expected at least ${expectedNames.length} installed agents under ${agentsDir}, got ${installedFiles.length}`);
for (const name of expectedNames) {
assert.ok(installedFiles.includes(`${name}.md`), `${name}.md must be installed under ${agentsDir}`);
}
// A couple of named agents, explicitly, spanning both source frontmatter
// styles (single-line `tools:` vs YAML block `tools:`) to exercise
// parseFrontmatterTools' dual-format tolerance end to end.
for (const known of ['msd-code-reviewer', 'msd-security-auditor', 'msd-nyquist-auditor']) {
const filePath = path.join(agentsDir, `${known}.md`);
assert.ok(fs.existsSync(filePath), `${filePath} must exist`);
const content = fs.readFileSync(filePath, 'utf8');
const fm = parseFrontmatter(content);
assert.ok(fm, `${known}.md must have YAML frontmatter — resolvable by Qwen's subagent loader`);
assert.match(fm, /^name:\s*\S+/m, `${known}.md frontmatter must declare name:`);
assert.match(fm, /^description:\s*\S/m, `${known}.md frontmatter must declare description:`);
// tools:, when present, must be a YAML block list (Qwen's documented
// schema — https://qwenlm.github.io/qwen-code-docs — not Claude's
// single-line comma-separated string.
if (/^tools:/m.test(fm)) {
assert.match(fm, /^tools:\r?\n(?: {2}- .+\r?\n?)+/m,
`${known}.md tools: must be emitted as a YAML block list`);
}
// color: is a documented Claude-Code-compatibility field in Qwen's
// native subagent schema — it must be preserved, not dropped.
assert.match(fm, /^color:\s*\S+/m, `${known}.md frontmatter must carry a preserved color: field`);
// No branding residue from the Claude Code source.
assert.ok(!content.includes('CLAUDE.md'), `${known}.md must not contain residual "CLAUDE.md"`);
assert.ok(!content.includes('Claude Code'), `${known}.md must not contain residual "Claude Code"`);
assert.ok(!content.includes('.claude/'), `${known}.md must not contain residual ".claude/"`);
}
});
}
// -- UPGRADE 2: SubagentStart hook fires symmetrically with SubagentStop ----
test('qwen --global: settings.json SubagentStart mirrors SubagentStop (UPGRADE 2)', (t) => {
const { configDir, root } = runMinimalInstall({ runtime: 'qwen', scope: 'global' });
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'));
const subagentStart = settings.hooks && settings.hooks.SubagentStart;
const subagentStop = settings.hooks && settings.hooks.SubagentStop;
assert.ok(Array.isArray(subagentStart) && subagentStart.length > 0,
'settings.hooks.SubagentStart must exist and be non-empty');
assert.ok(Array.isArray(subagentStop) && subagentStop.length > 0,
'settings.hooks.SubagentStop must exist and be non-empty');
const startEntry = subagentStart[0].hooks[0];
const stopEntry = subagentStop[0].hooks[0];
assert.equal(startEntry.command, stopEntry.command, 'SubagentStart must wire the same command as SubagentStop');
assert.equal(startEntry.timeout, 10);
assert.equal(stopEntry.timeout, 10);
});
test('a non-qwen runtime that does not declare SubagentStart does NOT get a SubagentStart hook (descriptor-gated, not global)', (t) => {
const { configDir, root } = runMinimalInstall({ runtime: 'claude', scope: 'global' });
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'));
// claude DOES get SubagentStop (sanity — proves hooks are wired at all for
// this runtime) but must NOT get SubagentStart — claude's
// extendedHookEvents omits it, proving the loop extension is
// descriptor-gated, not a global default applied to every runtime.
assert.ok(
settings.hooks && Array.isArray(settings.hooks.SubagentStop) && settings.hooks.SubagentStop.length > 0,
'claude must have SubagentStop wired (sanity — proves hooks ARE configured)',
);
assert.ok(
!settings.hooks || settings.hooks.SubagentStart === undefined,
"claude must NOT have SubagentStart wired — it is not in claude's extendedHookEvents",
);
});
// -- boundary/negative: extendedHookEvents is exactly the 4 documented events
test('capabilities/qwen/capability.json extendedHookEvents contains exactly the 4 documented events', () => {
const events = QWEN_CAP.runtime.extendedHookEvents;
assert.deepEqual(events, ['SubagentStop', 'Stop', 'PreCompact', 'SubagentStart']);
assert.equal(events.length, 4);
});